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

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

2

3===============================================================================

4

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

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

7

8 This file is part of CamCOPS.

9

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

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

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

13 (at your option) any later version.

14

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

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

17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the

18 GNU General Public License for more details.

19

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

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

22

23===============================================================================

24"""

25

26 import os

27 import tempfile

28 from typing import Any, Dict, Generator

29 from unittest import mock, TestCase

30

31 from pandas import DataFrame

32 import pendulum

33 import redcap

34

35 from camcops_server.cc_modules.cc_constants import ConfigParamExportRecipient

36 from camcops_server.cc_modules.cc_exportmodels import (

37 ExportedTask,

38 ExportedTaskRedcap,

39)

40 from camcops_server.cc_modules.cc_exportrecipient import ExportRecipient

41 from camcops_server.cc_modules.cc_exportrecipientinfo import (

42 ExportRecipientInfo,

43)

44 from camcops_server.cc_modules.cc_redcap import (

45 MISSING_EVENT_TAG_OR_ATTRIBUTE,

46 RedcapExportException,

47 RedcapFieldmap,

48 RedcapNewRecordUploader,

49 RedcapRecordStatus,

50 RedcapTaskExporter,

51)

52 from camcops_server.cc_modules.cc_testfactories import (

53 NHSPatientIdNumFactory,

54 PatientFactory,

55)

56 from camcops_server.cc_modules.cc_unittest import DemoRequestTestCase

57 from camcops_server.tasks.tests.factories import (

58 APEQCPFTPerinatalFactory,

59 BmiFactory,

60 KhandakerMojoMedicationTherapyFactory,

61 Phq9Factory,

62)

63

64# =============================================================================

65# Unit testing

66# =============================================================================

67

68

69 class MockProject(mock.Mock):

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

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

72

73 self.export_project_info = mock.Mock()

74 self.export_records = mock.Mock()

75 self.generate_next_record_name = mock.Mock()

76 self.import_file = mock.Mock()

77 self.import_records = mock.Mock()

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

79

80

81 class MockRedcapTaskExporter(RedcapTaskExporter):

82 def __init__(self) -> None:

83 mock_project = MockProject()

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

85

86 config = mock.Mock()

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

88

89

90 class MockRedcapNewRecordUploader(RedcapNewRecordUploader):

91 # noinspection PyMissingConstructor

92 def __init__(self) -> None:

93 self.req = mock.Mock()

94 self.project = MockProject()

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

96

97

98 class RedcapExporterTests(TestCase):

99 def test_next_instance_id_converted_to_int(self) -> None:

100 import numpy

101

102 records = DataFrame(

103 {

104 "record_id": ["1", "1", "1", "1", "1"],

105 "redcap_repeat_instrument": [

106 "bmi",

107 "bmi",

108 "bmi",

109 "bmi",

110 "bmi",

111 ],

112 "redcap_repeat_instance": [

113 numpy.float64(1.0),

114 numpy.float64(2.0),

115 numpy.float64(3.0),

116 numpy.float64(4.0),

117 numpy.float64(5.0),

118 ],

119 }

120 )

121

122 next_instance_id = RedcapTaskExporter._get_next_instance_id(

123 records, "bmi", "record_id", "1"

124 )

125

126 self.assertEqual(next_instance_id, 6)

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

128

129

130 class RedcapExportErrorTests(TestCase):

131 def test_raises_when_fieldmap_has_unknown_symbols(self) -> None:

132 exporter = MockRedcapNewRecordUploader()

133

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

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

136

137 field_dict: Dict[str, Any] = {}

138

139 with self.assertRaises(RedcapExportException) as cm:

140 exporter.transform_fields(field_dict, task, fieldmap)

141

142 message = str(cm.exception)

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

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

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

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

147

148 def test_raises_when_fieldmap_empty_in_config(self) -> None:

149

150 exporter = MockRedcapTaskExporter()

151

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

153 with self.assertRaises(RedcapExportException) as cm:

154 exporter.get_fieldmap_filename(recipient)

155

156 message = str(cm.exception)

157 self.assertIn(

158 f"{ConfigParamExportRecipient.REDCAP_FIELDMAP_FILENAME} "

159 f"is empty in the config file",

160 message,

161 )

162

163 def test_raises_when_fieldmap_not_set_in_config(self) -> None:

164

165 exporter = MockRedcapTaskExporter()

166

167 recipient = mock.Mock(redcap_fieldmap_filename=None)

168 with self.assertRaises(RedcapExportException) as cm:

169 exporter.get_fieldmap_filename(recipient)

170

171 message = str(cm.exception)

172 self.assertIn(

173 f"{ConfigParamExportRecipient.REDCAP_FIELDMAP_FILENAME} "

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

175 message,

176 )

177

178 def test_raises_when_error_from_redcap_on_import(self) -> None:

179 exporter = MockRedcapNewRecordUploader()

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

181 "Something went wrong"

182 )

183

184 with self.assertRaises(RedcapExportException) as cm:

185 record: Dict[str, Any] = {}

186 exporter.upload_record(record)

187 message = str(cm.exception)

188

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

190

191 def test_raises_when_error_from_redcap_on_init(self) -> None:

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

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

194

195 with self.assertRaises(RedcapExportException) as cm:

196 exporter = RedcapTaskExporter()

197 recipient = mock.Mock()

198 exporter.get_project(recipient)

199

200 message = str(cm.exception)

201

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

203

204 def test_raises_when_field_not_a_file_field(self) -> None:

205 exporter = MockRedcapNewRecordUploader()

206 exporter.project.import_file.side_effect = ValueError(

207 "Error with file field"

208 )

209

210 task = mock.Mock()

211

212 with self.assertRaises(RedcapExportException) as cm:

213 record_id = 1

214 repeat_instance = 1

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

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

217 message = str(cm.exception)

218

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

220

221 def test_raises_when_error_from_redcap_on_import_file(self) -> None:

222 exporter = MockRedcapNewRecordUploader()

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

224 "Something went wrong"

225 )

226

227 task = mock.Mock()

228

229 with self.assertRaises(RedcapExportException) as cm:

230 record_id = 1

231 repeat_instance = 1

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

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

234 message = str(cm.exception)

235

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

237

238

239 class RedcapFieldmapTests(TestCase):

240 def test_raises_when_xml_file_missing(self) -> None:

241 with self.assertRaises(RedcapExportException) as cm:

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

243

244 message = str(cm.exception)

245

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

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

248

249 def test_raises_when_fieldmap_missing(self) -> None:

250 with tempfile.NamedTemporaryFile(

251 mode="w", suffix="xml"

252 ) as fieldmap_file:

253 fieldmap_file.write(

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

255<someothertag></someothertag>

256"""

257 )

258 fieldmap_file.flush()

259

260 with self.assertRaises(RedcapExportException) as cm:

261 RedcapFieldmap(fieldmap_file.name)

262

263 message = str(cm.exception)

264 self.assertIn(

265 (

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

267 "'someothertag'"

268 ),

269 message,

270 )

271 self.assertIn(fieldmap_file.name, message)

272

273 def test_raises_when_root_tag_missing(self) -> None:

274 with tempfile.NamedTemporaryFile(

275 mode="w", suffix="xml"

276 ) as fieldmap_file:

277 fieldmap_file.write(

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

279"""

280 )

281 fieldmap_file.flush()

282

283 with self.assertRaises(RedcapExportException) as cm:

284 RedcapFieldmap(fieldmap_file.name)

285

286 message = str(cm.exception)

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

288 self.assertIn(fieldmap_file.name, message)

289

290 def test_raises_when_patient_missing(self) -> None:

291 with tempfile.NamedTemporaryFile(

292 mode="w", suffix="xml"

293 ) as fieldmap_file:

294 fieldmap_file.write(

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

296 <fieldmap>

297 </fieldmap>

298 """

299 )

300 fieldmap_file.flush()

301

302 with self.assertRaises(RedcapExportException) as cm:

303 RedcapFieldmap(fieldmap_file.name)

304

305 message = str(cm.exception)

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

307 self.assertIn(fieldmap_file.name, message)

308

309 def test_raises_when_patient_missing_attributes(self) -> None:

310 with tempfile.NamedTemporaryFile(

311 mode="w", suffix="xml"

312 ) as fieldmap_file:

313 fieldmap_file.write(

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

315 <fieldmap>

316 <patient />

317 </fieldmap>

318 """

319 )

320 fieldmap_file.flush()

321

322 with self.assertRaises(RedcapExportException) as cm:

323 RedcapFieldmap(fieldmap_file.name)

324

325 message = str(cm.exception)

326 self.assertIn(

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

328 )

329 self.assertIn(fieldmap_file.name, message)

330

331 def test_raises_when_record_missing(self) -> None:

332 with tempfile.NamedTemporaryFile(

333 mode="w", suffix="xml"

334 ) as fieldmap_file:

335 fieldmap_file.write(

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

337 <fieldmap>

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

339 </fieldmap>

340 """ # noqa: E501

341 )

342 fieldmap_file.flush()

343

344 with self.assertRaises(RedcapExportException) as cm:

345 RedcapFieldmap(fieldmap_file.name)

346

347 message = str(cm.exception)

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

349 self.assertIn(fieldmap_file.name, message)

350

351 def test_raises_when_record_missing_attributes(self) -> None:

352 with tempfile.NamedTemporaryFile(

353 mode="w", suffix="xml"

354 ) as fieldmap_file:

355 fieldmap_file.write(

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

357 <fieldmap>

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

359 <record />

360 </fieldmap>

361 """ # noqa: E501

362 )

363 fieldmap_file.flush()

364

365 with self.assertRaises(RedcapExportException) as cm:

366 RedcapFieldmap(fieldmap_file.name)

367

368 message = str(cm.exception)

369 self.assertIn(

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

371 )

372 self.assertIn(fieldmap_file.name, message)

373

374 def test_raises_when_instruments_missing(self) -> None:

375 with tempfile.NamedTemporaryFile(

376 mode="w", suffix="xml"

377 ) as fieldmap_file:

378 fieldmap_file.write(

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

380 <fieldmap>

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

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

383 </fieldmap>

384 """ # noqa: E501

385 )

386 fieldmap_file.flush()

387

388 with self.assertRaises(RedcapExportException) as cm:

389 RedcapFieldmap(fieldmap_file.name)

390

391 message = str(cm.exception)

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

393 self.assertIn(fieldmap_file.name, message)

394

395 def test_raises_when_instruments_missing_attributes(self) -> None:

396 with tempfile.NamedTemporaryFile(

397 mode="w", suffix="xml"

398 ) as fieldmap_file:

399 fieldmap_file.write(

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

401 <fieldmap>

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

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

404 <instruments>

405 <instrument />

406 </instruments>

407 </fieldmap>

408 """ # noqa: E501

409 )

410 fieldmap_file.flush()

411

412 with self.assertRaises(RedcapExportException) as cm:

413 RedcapFieldmap(fieldmap_file.name)

414

415 message = str(cm.exception)

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

417 self.assertIn(fieldmap_file.name, message)

418

419 def test_raises_when_file_fields_missing_attributes(self) -> None:

420 with tempfile.NamedTemporaryFile(

421 mode="w", suffix="xml"

422 ) as fieldmap_file:

423 fieldmap_file.write(

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

425 <fieldmap>

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

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

428 <instruments>

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

430 <files>

431 <field />

432 </files>

433 </instrument>

434 </instruments>

435 </fieldmap>

436 """ # noqa: E501

437 )

438 fieldmap_file.flush()

439

440 with self.assertRaises(RedcapExportException) as cm:

441 RedcapFieldmap(fieldmap_file.name)

442

443 message = str(cm.exception)

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

445 self.assertIn(fieldmap_file.name, message)

446

447 def test_raises_when_fields_missing_attributes(self) -> None:

448 with tempfile.NamedTemporaryFile(

449 mode="w", suffix="xml"

450 ) as fieldmap_file:

451 fieldmap_file.write(

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

453 <fieldmap>

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

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

456 <instruments>

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

458 <fields>

459 <field />

460 </fields>

461 </instrument>

462 </instruments>

463 </fieldmap>

464 """ # noqa: E501

465 )

466 fieldmap_file.flush()

467

468 with self.assertRaises(RedcapExportException) as cm:

469 RedcapFieldmap(fieldmap_file.name)

470

471 message = str(cm.exception)

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

473 self.assertIn(fieldmap_file.name, message)

474

475

476# =============================================================================

477# Integration testing

478# =============================================================================

479

480

481 class RedcapExportTestCase(DemoRequestTestCase):

482 fieldmap = ""

483

484 def setUp(self) -> None:

485 super().setUp()

486

487 self.patient = PatientFactory()

488 self.patient_idnum = NHSPatientIdNumFactory(patient=self.patient)

489

490 recipientinfo = ExportRecipientInfo()

491

492 self.recipient = ExportRecipient(recipientinfo)

493 self.recipient.primary_idnum = self.patient_idnum.which_idnum

494

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

496 self.recipient.id = 1

497 self.recipient.recipient_name = "test"

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

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

500 )

501 self.write_fieldmaps(self.recipient.redcap_fieldmap_filename)

502

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

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

505 f.write(self.fieldmap)

506

507

508 class BmiRedcapValidFieldmapTestCase(RedcapExportTestCase):

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

510<fieldmap>

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

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

513 <instruments>

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

515 <fields>

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

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

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

519 </fields>

520 </instrument>

521 </instruments>

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

523

524

525 class BmiRedcapExportTests(BmiRedcapValidFieldmapTestCase):

526 """

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

528 related to the BMI task

529 """

530

531 def setUp(self) -> None:

532 super().setUp()

533

534 self.task = BmiFactory(

535 patient=self.patient,

536 height_m=1.83,

537 mass_kg=67.57,

538 when_created=pendulum.parse("2010-07-07"),

539 )

540

541 def test_record_exported(self) -> None:

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

543 exported_task_redcap = ExportedTaskRedcap(exported_task)

544

545 exporter = MockRedcapTaskExporter()

546 project = exporter.get_project()

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

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

549 project.export_project_info.return_value = {

550 "record_autonumbering_enabled": 1

551 }

552

553 exporter.export_task(self.req, exported_task_redcap)

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

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

556 self.assertEqual(exported_task_redcap.redcap_instance_id, 1)

557

558 args, kwargs = project.export_records.call_args

559

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

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

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

563

564 # Initial call with original record

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

566

567 rows = args[0]

568 record = rows[0]

569

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

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

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

573 self.assertEqual(

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

575 )

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

577

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

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

580

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

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

583

584 # Second call with updated patient ID

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

586 rows = args[0]

587 record = rows[0]

588

589 self.assertEqual(record["patient_id"], self.patient_idnum.idnum_value)

590

591 def test_record_exported_with_non_integer_id(self) -> None:

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

593 exported_task_redcap = ExportedTaskRedcap(exported_task)

594

595 exporter = MockRedcapTaskExporter()

596 project = exporter.get_project()

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

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

599 project.export_project_info.return_value = {

600 "record_autonumbering_enabled": 1

601 }

602

603 exporter.export_task(self.req, exported_task_redcap)

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

605

606 def test_record_id_generated_when_no_autonumbering(self) -> None:

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

608 exported_task_redcap = ExportedTaskRedcap(exported_task)

609

610 exporter = MockRedcapTaskExporter()

611 project = exporter.get_project()

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

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

614 project.export_project_info.return_value = {

615 "record_autonumbering_enabled": 0

616 }

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

618

619 exporter.export_task(self.req, exported_task_redcap)

620

621 # Initial call with original record

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

623

624 rows = args[0]

625 record = rows[0]

626

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

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

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

630

631 def test_record_imported_when_no_existing_records(self) -> None:

632 exporter = MockRedcapTaskExporter()

633 project = exporter.get_project()

634 project.export_records.return_value = DataFrame()

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

636 project.export_project_info.return_value = {

637 "record_autonumbering_enabled": 1

638 }

639

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

641 exported_task_redcap = ExportedTaskRedcap(exported_task)

642 exporter.export_task(self.req, exported_task_redcap)

643

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

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

646 self.assertEqual(exported_task_redcap.redcap_instance_id, 1)

647

648

649 class BmiRedcapUpdateTests(BmiRedcapValidFieldmapTestCase):

650 def setUp(self) -> None:

651 super().setUp()

652

653 self.task1 = BmiFactory(

654 patient=self.patient,

655 )

656 self.task2 = BmiFactory(

657 patient=self.patient,

658 )

659

660 def test_existing_record_id_used_for_update(self) -> None:

661 exporter = MockRedcapTaskExporter()

662 project = exporter.get_project()

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

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

665 project.export_project_info.return_value = {

666 "record_autonumbering_enabled": 1

667 }

668

669 exported_task1 = ExportedTask(

670 task=self.task1, recipient=self.recipient

671 )

672 exported_task_redcap1 = ExportedTaskRedcap(exported_task1)

673 exporter.export_task(self.req, exported_task_redcap1)

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

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

676 self.assertEqual(exported_task_redcap1.redcap_instance_id, 1)

677

678 project.export_records.return_value = DataFrame(

679 {

680 "record_id": ["123"],

681 "patient_id": [self.patient_idnum.idnum_value],

682 "redcap_repeat_instrument": ["bmi"],

683 "redcap_repeat_instance": [1],

684 }

685 )

686 exported_task2 = ExportedTask(

687 task=self.task2, recipient=self.recipient

688 )

689 exported_task_redcap2 = ExportedTaskRedcap(exported_task2)

690

691 exporter.export_task(self.req, exported_task_redcap2)

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

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

694 self.assertEqual(exported_task_redcap2.redcap_instance_id, 2)

695

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

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

698

699 rows = args[0]

700 record = rows[0]

701

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

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

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

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

706

707

708 class Phq9RedcapExportTests(RedcapExportTestCase):

709 """

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

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

712 field.

713 """

714

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

716<fieldmap>

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

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

719 <instruments>

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

721 <fields>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

736 </fields>

737 </instrument>

738 </instruments>

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

740

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

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

743

744 def setUp(self) -> None:

745 super().setUp()

746

747 self.task = Phq9Factory(

748 patient=self.patient,

749 q1=0,

750 q2=1,

751 q3=2,

752 q4=3,

753 q5=0,

754 q6=1,

755 q7=2,

756 q8=3,

757 q9=0,

758 q10=3,

759 when_created=pendulum.parse("2010-07-07"),

760 )

761

762 def test_record_exported(self) -> None:

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

764 exported_task_redcap = ExportedTaskRedcap(exported_task)

765

766 exporter = MockRedcapTaskExporter()

767 project = exporter.get_project()

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

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

770 project.export_project_info.return_value = {

771 "record_autonumbering_enabled": 1

772 }

773

774 exporter.export_task(self.req, exported_task_redcap)

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

776 self.assertEqual(

777 exported_task_redcap.redcap_instrument_name,

778 "patient_health_questionnaire_9",

779 )

780 self.assertEqual(exported_task_redcap.redcap_instance_id, 1)

781

782 # Initial call with new record

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

784

785 rows = args[0]

786 record = rows[0]

787

788 self.assertEqual(

789 record["redcap_repeat_instrument"],

790 "patient_health_questionnaire_9",

791 )

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

793 self.assertEqual(

794 record["patient_health_questionnaire_9_complete"],

795 RedcapRecordStatus.COMPLETE.value,

796 )

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

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

799 self.assertEqual(record["phq9_first_name"], self.patient.forename)

800 self.assertEqual(record["phq9_last_name"], self.patient.surname)

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

802

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

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

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

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

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

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

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

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

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

812

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

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

815

816 # Second call with patient ID

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

818

819 rows = args[0]

820 record = rows[0]

821 self.assertEqual(record["patient_id"], self.patient_idnum.idnum_value)

822

823

824 class MedicationTherapyRedcapExportTests(RedcapExportTestCase):

825 """

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

827 related to the KhandakerMojoMedicationTherapy task

828 """

829

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

831<fieldmap>

832 <event name="event_1_arm_1" />

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

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

835 <instruments>

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

837 <files>

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

839 </files>

840 </instrument>

841 </instruments>

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

843

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

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

846 self.id_sequence = self.get_id()

847

848 @staticmethod

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

850 i = 1

851

852 while True:

853 yield i

854 i += 1

855

856 def setUp(self) -> None:

857 super().setUp()

858

859 self.task = KhandakerMojoMedicationTherapyFactory(

860 patient=self.patient,

861 )

862

863 def test_record_exported(self) -> None:

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

865 exported_task_redcap = ExportedTaskRedcap(exported_task)

866

867 exporter = MockRedcapTaskExporter()

868 project = exporter.get_project()

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

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

871 project.export_project_info.return_value = {

872 "record_autonumbering_enabled": 1

873 }

874

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

876 # the file will already have been closed by then

877 # noinspection PyUnusedLocal

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

879 # record, field, fname, fobj

880 file_obj = import_file_args[3]

881 read_pdf_bytes.pdf_header = file_obj.read(5)

882

883 project.import_file.side_effect = read_pdf_bytes

884

885 exporter.export_task(self.req, exported_task_redcap)

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

887 self.assertEqual(

888 exported_task_redcap.redcap_instrument_name, "medication_table"

889 )

890 self.assertEqual(exported_task_redcap.redcap_instance_id, 1)

891

892 args, kwargs = project.import_file.call_args

893

894 record_id = args[0]

895 fieldname = args[1]

896 filename = args[2]

897

898 self.assertEqual(record_id, "123")

899 self.assertEqual(fieldname, "medtbl_medication_items")

900 self.assertEqual(

901 filename,

902 "khandaker_mojo_medicationtherapy_123_medtbl_medication_items",

903 )

904

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

906 # noinspection PyUnresolvedReferences

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

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

909

910

911 class MultipleTaskRedcapExportTests(RedcapExportTestCase):

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

913<fieldmap>

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

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

916 <instruments>

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

918 <fields>

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

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

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

922 </fields>

923 </instrument>

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

925 <files>

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

927 </files>

928 </instrument>

929 </instruments>

930</fieldmap>

931""" # noqa: E501

932

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

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

935

936 def setUp(self) -> None:

937 super().setUp()

938

939 self.mojo_task = KhandakerMojoMedicationTherapyFactory(

940 patient=self.patient

941 )

942

943 self.bmi_task = BmiFactory(patient=self.patient)

944

945 def test_instance_ids_on_different_tasks_in_same_record(self) -> None:

946 exporter = MockRedcapTaskExporter()

947 project = exporter.get_project()

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

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

950 project.export_project_info.return_value = {

951 "record_autonumbering_enabled": 1

952 }

953

954 exported_task_mojo = ExportedTask(

955 task=self.mojo_task, recipient=self.recipient

956 )

957 exported_task_redcap_mojo = ExportedTaskRedcap(exported_task_mojo)

958 exporter.export_task(self.req, exported_task_redcap_mojo)

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

960 args, kwargs = project.import_file.call_args

961

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

963

964 project.export_records.return_value = DataFrame(

965 {

966 "record_id": ["123"],

967 "patient_id": [self.patient_idnum.idnum_value],

968 "redcap_repeat_instrument": [

969 "khandaker_mojo_medicationtherapy"

970 ],

971 "redcap_repeat_instance": [1],

972 }

973 )

974 exported_task_bmi = ExportedTask(

975 task=self.bmi_task, recipient=self.recipient

976 )

977 exported_task_redcap_bmi = ExportedTaskRedcap(exported_task_bmi)

978

979 exporter.export_task(self.req, exported_task_redcap_bmi)

980

981 # Import of second task, but is first instance

982 # (third call to import_records)

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

984

985 rows = args[0]

986 record = rows[0]

987

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

989

990 def test_imported_into_different_events(self) -> None:

991 exporter = MockRedcapTaskExporter()

992 project = exporter.get_project()

993

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

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

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

997 project.export_project_info.return_value = {

998 "record_autonumbering_enabled": 1

999 }

1000

1001 exported_task_mojo = ExportedTask(

1002 task=self.mojo_task, recipient=self.recipient

1003 )

1004 exported_task_redcap_mojo = ExportedTaskRedcap(exported_task_mojo)

1005

1006 exporter.export_task(self.req, exported_task_redcap_mojo)

1007

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

1009 rows = args[0]

1010 record = rows[0]

1011

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

1013 args, kwargs = project.import_file.call_args

1014

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

1016

1017 exported_task_bmi = ExportedTask(

1018 task=self.bmi_task, recipient=self.recipient

1019 )

1020 exported_task_redcap_bmi = ExportedTaskRedcap(exported_task_bmi)

1021

1022 exporter.export_task(self.req, exported_task_redcap_bmi)

1023

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

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

1026 rows = args[0]

1027 record = rows[0]

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

1029

1030

1031 class BadConfigurationRedcapTests(RedcapExportTestCase):

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

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

1034

1035 def setUp(self) -> None:

1036 super().setUp()

1037

1038 self.task = BmiFactory(patient=self.patient)

1039

1040

1041 class MissingInstrumentRedcapTests(BadConfigurationRedcapTests):

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

1043<fieldmap>

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

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

1046 <instruments>

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

1048 <fields>

1049 </fields>

1050 </instrument>

1051 </instruments>

1052</fieldmap>"""

1053

1054 def test_raises_when_instrument_missing_from_fieldmap(self) -> None:

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

1056 exported_task_redcap = ExportedTaskRedcap(exported_task)

1057

1058 exporter = MockRedcapTaskExporter()

1059 project = exporter.get_project()

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

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

1062

1063 with self.assertRaises(RedcapExportException) as cm:

1064 exporter.export_task(self.req, exported_task_redcap)

1065

1066 message = str(cm.exception)

1067 self.assertIn(

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

1069 )

1070

1071

1072 class IncorrectRecordIdRedcapTests(BadConfigurationRedcapTests):

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

1074<fieldmap>

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

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

1077 <instruments>

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

1079 <fields>

1080 </fields>

1081 </instrument>

1082 </instruments>

1083</fieldmap>"""

1084

1085 def test_raises_when_record_id_is_incorrect(self) -> None:

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

1087 exported_task_redcap = ExportedTaskRedcap(exported_task)

1088

1089 exporter = MockRedcapTaskExporter()

1090 project = exporter.get_project()

1091 project.export_records.return_value = DataFrame(

1092 {

1093 "record_id": ["123"],

1094 "patient_id": [self.patient_idnum.idnum_value],

1095 "redcap_repeat_instrument": ["bmi"],

1096 "redcap_repeat_instance": [1],

1097 }

1098 )

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

1100 project.export_project_info.return_value = {

1101 "record_autonumbering_enabled": 1

1102 }

1103

1104 with self.assertRaises(RedcapExportException) as cm:

1105 exporter.export_task(self.req, exported_task_redcap)

1106

1107 message = str(cm.exception)

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

1109

1110

1111 class IncorrectPatientIdRedcapTests(BadConfigurationRedcapTests):

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

1113<fieldmap>

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

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

1116 <instruments>

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

1118 <fields>

1119 </fields>

1120 </instrument>

1121 </instruments>

1122</fieldmap>"""

1123

1124 def test_raises_when_patient_id_is_incorrect(self) -> None:

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

1126 exported_task_redcap = ExportedTaskRedcap(exported_task)

1127

1128 exporter = MockRedcapTaskExporter()

1129 project = exporter.get_project()

1130 project.export_records.return_value = DataFrame(

1131 {

1132 "record_id": ["123"],

1133 "patient_id": [self.patient_idnum.idnum_value],

1134 "redcap_repeat_instrument": ["bmi"],

1135 "redcap_repeat_instance": [1],

1136 }

1137 )

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

1139 project.export_project_info.return_value = {

1140 "record_autonumbering_enabled": 1

1141 }

1142

1143 with self.assertRaises(RedcapExportException) as cm:

1144 exporter.export_task(self.req, exported_task_redcap)

1145

1146 message = str(cm.exception)

1147 self.assertIn(

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

1149 )

1150

1151

1152 class MissingPatientInstrumentRedcapTests(BadConfigurationRedcapTests):

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

1154<fieldmap>

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

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

1157 <instruments>

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

1159 <fields>

1160 </fields>

1161 </instrument>

1162 </instruments>

1163</fieldmap>"""

1164

1165 def test_raises_when_instrument_is_missing(self) -> None:

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

1167 exported_task_redcap = ExportedTaskRedcap(exported_task)

1168

1169 exporter = MockRedcapTaskExporter()

1170 project = exporter.get_project()

1171 project.export_records.side_effect = redcap.RedcapError(

1172 "Something went wrong"

1173 )

1174

1175 with self.assertRaises(RedcapExportException) as cm:

1176 exporter.export_task(self.req, exported_task_redcap)

1177

1178 message = str(cm.exception)

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

1180

1181

1182 class MissingEventRedcapTests(BadConfigurationRedcapTests):

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

1184<fieldmap>

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

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

1187 <instruments>

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

1189 <fields>

1190 </fields>

1191 </instrument>

1192 </instruments>

1193</fieldmap>"""

1194

1195 def test_raises_for_longitudinal_project(self) -> None:

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

1197 exported_task_redcap = ExportedTaskRedcap(exported_task)

1198

1199 exporter = MockRedcapTaskExporter()

1200 project = exporter.get_project()

1201

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

1203

1204 with self.assertRaises(RedcapExportException) as cm:

1205 exporter.export_task(self.req, exported_task_redcap)

1206

1207 message = str(cm.exception)

1208 self.assertEqual(MISSING_EVENT_TAG_OR_ATTRIBUTE, message)

1209

1210

1211 class MissingInstrumentEventRedcapTests(BadConfigurationRedcapTests):

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

1213<fieldmap>

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

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

1216 <instruments>

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

1218 <fields>

1219 </fields>

1220 </instrument>

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

1222 <fields>

1223 </fields>

1224 </instrument>

1225 </instruments>

1226</fieldmap>"""

1227

1228 def test_raises_when_instrument_missing_event(self) -> None:

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

1230 exported_task_redcap = ExportedTaskRedcap(exported_task)

1231

1232 exporter = MockRedcapTaskExporter()

1233 project = exporter.get_project()

1234

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

1236

1237 with self.assertRaises(RedcapExportException) as cm:

1238 exporter.export_task(self.req, exported_task_redcap)

1239

1240 message = str(cm.exception)

1241 self.assertEqual(MISSING_EVENT_TAG_OR_ATTRIBUTE, message)

1242

1243

1244 class AnonymousTaskRedcapTests(RedcapExportTestCase):

1245 def setUp(self) -> None:

1246 super().setUp()

1247

1248 self.task = APEQCPFTPerinatalFactory()

1249

1250 def test_raises_when_task_is_anonymous(self) -> None:

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

1252 exported_task_redcap = ExportedTaskRedcap(exported_task)

1253

1254 exporter = MockRedcapTaskExporter()

1255

1256 with self.assertRaises(RedcapExportException) as cm:

1257 exporter.export_task(self.req, exported_task_redcap)

1258

1259 message = str(cm.exception)

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