Coverage for cc_modules/tests/cc_redcap_tests.py: 19%
558 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-30 13:48 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-30 13:48 +0000
1"""camcops_server/cc_modules/tests/cc_redcap_tests.py
3===============================================================================
5 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
6 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
8 This file is part of CamCOPS.
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.
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.
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/>.
23===============================================================================
24"""
26import os
27import tempfile
28from typing import Any, Dict, Generator
29from unittest import mock, TestCase
31from pandas import DataFrame
32import pendulum
33import redcap
35from camcops_server.cc_modules.cc_constants import ConfigParamExportRecipient
36from camcops_server.cc_modules.cc_exportmodels import (
37 ExportedTask,
38 ExportedTaskRedcap,
39)
40from camcops_server.cc_modules.cc_exportrecipient import ExportRecipient
41from camcops_server.cc_modules.cc_exportrecipientinfo import (
42 ExportRecipientInfo,
43)
44from camcops_server.cc_modules.cc_redcap import (
45 MISSING_EVENT_TAG_OR_ATTRIBUTE,
46 RedcapExportException,
47 RedcapFieldmap,
48 RedcapNewRecordUploader,
49 RedcapRecordStatus,
50 RedcapTaskExporter,
51)
52from camcops_server.cc_modules.cc_testfactories import (
53 NHSPatientIdNumFactory,
54 PatientFactory,
55)
56from camcops_server.cc_modules.cc_unittest import DemoRequestTestCase
57from camcops_server.tasks.tests.factories import (
58 APEQCPFTPerinatalFactory,
59 BmiFactory,
60 KhandakerMojoMedicationTherapyFactory,
61 Phq9Factory,
62)
64# =============================================================================
65# Unit testing
66# =============================================================================
69class MockProject(mock.Mock):
70 def __init__(self, *args, **kwargs) -> None:
71 super().__init__(*args, **kwargs)
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)
81class MockRedcapTaskExporter(RedcapTaskExporter):
82 def __init__(self) -> None:
83 mock_project = MockProject()
84 self.get_project = mock.Mock(return_value=mock_project)
86 config = mock.Mock()
87 self.req = mock.Mock(config=config)
90class 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")
98class RedcapExporterTests(TestCase):
99 def test_next_instance_id_converted_to_int(self) -> None:
100 import numpy
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 )
122 next_instance_id = RedcapTaskExporter._get_next_instance_id(
123 records, "bmi", "record_id", "1"
124 )
126 self.assertEqual(next_instance_id, 6)
127 self.assertEqual(type(next_instance_id), int)
130class RedcapExportErrorTests(TestCase):
131 def test_raises_when_fieldmap_has_unknown_symbols(self) -> None:
132 exporter = MockRedcapNewRecordUploader()
134 task = mock.Mock(tablename="bmi")
135 fieldmap = {"pa_height": "sys.platform"}
137 field_dict: Dict[str, Any] = {}
139 with self.assertRaises(RedcapExportException) as cm:
140 exporter.transform_fields(field_dict, task, fieldmap)
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)
148 def test_raises_when_fieldmap_empty_in_config(self) -> None:
150 exporter = MockRedcapTaskExporter()
152 recipient = mock.Mock(redcap_fieldmap_filename="")
153 with self.assertRaises(RedcapExportException) as cm:
154 exporter.get_fieldmap_filename(recipient)
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 )
163 def test_raises_when_fieldmap_not_set_in_config(self) -> None:
165 exporter = MockRedcapTaskExporter()
167 recipient = mock.Mock(redcap_fieldmap_filename=None)
168 with self.assertRaises(RedcapExportException) as cm:
169 exporter.get_fieldmap_filename(recipient)
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 )
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 )
184 with self.assertRaises(RedcapExportException) as cm:
185 record: Dict[str, Any] = {}
186 exporter.upload_record(record)
187 message = str(cm.exception)
189 self.assertIn("Something went wrong", message)
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")
195 with self.assertRaises(RedcapExportException) as cm:
196 exporter = RedcapTaskExporter()
197 recipient = mock.Mock()
198 exporter.get_project(recipient)
200 message = str(cm.exception)
202 self.assertIn("Something went wrong", message)
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 )
210 task = mock.Mock()
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)
219 self.assertIn("Error with file field", message)
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 )
227 task = mock.Mock()
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)
236 self.assertIn("Something went wrong", message)
239class 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")
244 message = str(cm.exception)
246 self.assertIn("Unable to open fieldmap file", message)
247 self.assertIn("bmi.xml", message)
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()
260 with self.assertRaises(RedcapExportException) as cm:
261 RedcapFieldmap(fieldmap_file.name)
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)
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()
283 with self.assertRaises(RedcapExportException) as cm:
284 RedcapFieldmap(fieldmap_file.name)
286 message = str(cm.exception)
287 self.assertIn("There was a problem parsing", message)
288 self.assertIn(fieldmap_file.name, message)
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()
302 with self.assertRaises(RedcapExportException) as cm:
303 RedcapFieldmap(fieldmap_file.name)
305 message = str(cm.exception)
306 self.assertIn("'patient' is missing from", message)
307 self.assertIn(fieldmap_file.name, message)
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()
322 with self.assertRaises(RedcapExportException) as cm:
323 RedcapFieldmap(fieldmap_file.name)
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)
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()
344 with self.assertRaises(RedcapExportException) as cm:
345 RedcapFieldmap(fieldmap_file.name)
347 message = str(cm.exception)
348 self.assertIn("'record' is missing from", message)
349 self.assertIn(fieldmap_file.name, message)
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()
365 with self.assertRaises(RedcapExportException) as cm:
366 RedcapFieldmap(fieldmap_file.name)
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)
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()
388 with self.assertRaises(RedcapExportException) as cm:
389 RedcapFieldmap(fieldmap_file.name)
391 message = str(cm.exception)
392 self.assertIn("'instruments' tag is missing from", message)
393 self.assertIn(fieldmap_file.name, message)
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()
412 with self.assertRaises(RedcapExportException) as cm:
413 RedcapFieldmap(fieldmap_file.name)
415 message = str(cm.exception)
416 self.assertIn("'instrument' must have attributes: name, task", message)
417 self.assertIn(fieldmap_file.name, message)
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()
440 with self.assertRaises(RedcapExportException) as cm:
441 RedcapFieldmap(fieldmap_file.name)
443 message = str(cm.exception)
444 self.assertIn("'field' must have attributes: name, formula", message)
445 self.assertIn(fieldmap_file.name, message)
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()
468 with self.assertRaises(RedcapExportException) as cm:
469 RedcapFieldmap(fieldmap_file.name)
471 message = str(cm.exception)
472 self.assertIn("'field' must have attributes: name, formula", message)
473 self.assertIn(fieldmap_file.name, message)
476# =============================================================================
477# Integration testing
478# =============================================================================
481class RedcapExportTestCase(DemoRequestTestCase):
482 fieldmap = ""
484 def setUp(self) -> None:
485 super().setUp()
487 self.patient = PatientFactory()
488 self.patient_idnum = NHSPatientIdNumFactory(patient=self.patient)
490 recipientinfo = ExportRecipientInfo()
492 self.recipient = ExportRecipient(recipientinfo)
493 self.recipient.primary_idnum = self.patient_idnum.which_idnum
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)
503 def write_fieldmaps(self, filename: str) -> None:
504 with open(filename, "w") as f:
505 f.write(self.fieldmap)
508class 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
525class BmiRedcapExportTests(BmiRedcapValidFieldmapTestCase):
526 """
527 These are more of a test of the fieldmap code than anything
528 related to the BMI task
529 """
531 def setUp(self) -> None:
532 super().setUp()
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 )
541 def test_record_exported(self) -> None:
542 exported_task = ExportedTask(task=self.task, recipient=self.recipient)
543 exported_task_redcap = ExportedTaskRedcap(exported_task)
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 }
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)
558 args, kwargs = project.export_records.call_args
560 self.assertIn("bmi", kwargs["forms"])
561 self.assertIn("patient_record", kwargs["forms"])
562 self.assertIn("instrument_with_record_id", kwargs["forms"])
564 # Initial call with original record
565 args, kwargs = project.import_records.call_args_list[0]
567 rows = args[0]
568 record = rows[0]
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")
578 self.assertEqual(record["pa_height"], "1.8")
579 self.assertEqual(record["pa_weight"], "67.6")
581 self.assertEqual(kwargs["return_content"], "auto_ids")
582 self.assertTrue(kwargs["force_auto_number"])
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]
589 self.assertEqual(record["patient_id"], self.patient_idnum.idnum_value)
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)
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 }
603 exporter.export_task(self.req, exported_task_redcap)
604 self.assertEqual(exported_task_redcap.redcap_record_id, "15-123")
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)
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"
619 exporter.export_task(self.req, exported_task_redcap)
621 # Initial call with original record
622 args, kwargs = project.import_records.call_args_list[0]
624 rows = args[0]
625 record = rows[0]
627 self.assertEqual(record["record_id"], "15-29")
628 self.assertEqual(kwargs["return_content"], "count")
629 self.assertFalse(kwargs["force_auto_number"])
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 }
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)
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)
649class BmiRedcapUpdateTests(BmiRedcapValidFieldmapTestCase):
650 def setUp(self) -> None:
651 super().setUp()
653 self.task1 = BmiFactory(
654 patient=self.patient,
655 )
656 self.task2 = BmiFactory(
657 patient=self.patient,
658 )
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 }
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)
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)
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)
696 # Third call (after initial record and patient ID)
697 args, kwargs = project.import_records.call_args_list[2]
699 rows = args[0]
700 record = rows[0]
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"])
708class 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 """
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
741 def __init__(self, *args, **kwargs) -> None:
742 super().__init__(*args, **kwargs)
744 def setUp(self) -> None:
745 super().setUp()
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 )
762 def test_record_exported(self) -> None:
763 exported_task = ExportedTask(task=self.task, recipient=self.recipient)
764 exported_task_redcap = ExportedTaskRedcap(exported_task)
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 }
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)
782 # Initial call with new record
783 args, kwargs = project.import_records.call_args_list[0]
785 rows = args[0]
786 record = rows[0]
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")
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)
813 self.assertEqual(kwargs["return_content"], "auto_ids")
814 self.assertTrue(kwargs["force_auto_number"])
816 # Second call with patient ID
817 args, kwargs = project.import_records.call_args_list[1]
819 rows = args[0]
820 record = rows[0]
821 self.assertEqual(record["patient_id"], self.patient_idnum.idnum_value)
824class MedicationTherapyRedcapExportTests(RedcapExportTestCase):
825 """
826 These are more of a test of the file upload code than anything
827 related to the KhandakerMojoMedicationTherapy task
828 """
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
844 def __init__(self, *args, **kwargs) -> None:
845 super().__init__(*args, **kwargs)
846 self.id_sequence = self.get_id()
848 @staticmethod
849 def get_id() -> Generator[int, None, None]:
850 i = 1
852 while True:
853 yield i
854 i += 1
856 def setUp(self) -> None:
857 super().setUp()
859 self.task = KhandakerMojoMedicationTherapyFactory(
860 patient=self.patient,
861 )
863 def test_record_exported(self) -> None:
864 exported_task = ExportedTask(task=self.task, recipient=self.recipient)
865 exported_task_redcap = ExportedTaskRedcap(exported_task)
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 }
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)
883 project.import_file.side_effect = read_pdf_bytes
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)
892 args, kwargs = project.import_file.call_args
894 record_id = args[0]
895 fieldname = args[1]
896 filename = args[2]
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 )
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")
911class 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
933 def __init__(self, *args, **kwargs) -> None:
934 super().__init__(*args, **kwargs)
936 def setUp(self) -> None:
937 super().setUp()
939 self.mojo_task = KhandakerMojoMedicationTherapyFactory(
940 patient=self.patient
941 )
943 self.bmi_task = BmiFactory(patient=self.patient)
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 }
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
962 self.assertEqual(kwargs["repeat_instance"], 1)
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)
979 exporter.export_task(self.req, exported_task_redcap_bmi)
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]
985 rows = args[0]
986 record = rows[0]
988 self.assertEqual(record["redcap_repeat_instance"], 1)
990 def test_imported_into_different_events(self) -> None:
991 exporter = MockRedcapTaskExporter()
992 project = exporter.get_project()
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 }
1001 exported_task_mojo = ExportedTask(
1002 task=self.mojo_task, recipient=self.recipient
1003 )
1004 exported_task_redcap_mojo = ExportedTaskRedcap(exported_task_mojo)
1006 exporter.export_task(self.req, exported_task_redcap_mojo)
1008 args, kwargs = project.import_records.call_args_list[0]
1009 rows = args[0]
1010 record = rows[0]
1012 self.assertEqual(record["redcap_event_name"], "mojo_event")
1013 args, kwargs = project.import_file.call_args
1015 self.assertEqual(kwargs["event"], "mojo_event")
1017 exported_task_bmi = ExportedTask(
1018 task=self.bmi_task, recipient=self.recipient
1019 )
1020 exported_task_redcap_bmi = ExportedTaskRedcap(exported_task_bmi)
1022 exporter.export_task(self.req, exported_task_redcap_bmi)
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")
1031class BadConfigurationRedcapTests(RedcapExportTestCase):
1032 def __init__(self, *args, **kwargs) -> None:
1033 super().__init__(*args, **kwargs)
1035 def setUp(self) -> None:
1036 super().setUp()
1038 self.task = BmiFactory(patient=self.patient)
1041class 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>"""
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)
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"]
1063 with self.assertRaises(RedcapExportException) as cm:
1064 exporter.export_task(self.req, exported_task_redcap)
1066 message = str(cm.exception)
1067 self.assertIn(
1068 "Instrument for task 'bmi' is missing from the fieldmap", message
1069 )
1072class 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>"""
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)
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 }
1104 with self.assertRaises(RedcapExportException) as cm:
1105 exporter.export_task(self.req, exported_task_redcap)
1107 message = str(cm.exception)
1108 self.assertIn("Field 'my_record_id' does not exist in REDCap", message)
1111class 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>"""
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)
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 }
1143 with self.assertRaises(RedcapExportException) as cm:
1144 exporter.export_task(self.req, exported_task_redcap)
1146 message = str(cm.exception)
1147 self.assertIn(
1148 "Field 'my_patient_id' does not exist in REDCap", message
1149 )
1152class 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>"""
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)
1169 exporter = MockRedcapTaskExporter()
1170 project = exporter.get_project()
1171 project.export_records.side_effect = redcap.RedcapError(
1172 "Something went wrong"
1173 )
1175 with self.assertRaises(RedcapExportException) as cm:
1176 exporter.export_task(self.req, exported_task_redcap)
1178 message = str(cm.exception)
1179 self.assertIn("Something went wrong", message)
1182class 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>"""
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)
1199 exporter = MockRedcapTaskExporter()
1200 project = exporter.get_project()
1202 project.is_longitudinal = mock.Mock(return_value=True)
1204 with self.assertRaises(RedcapExportException) as cm:
1205 exporter.export_task(self.req, exported_task_redcap)
1207 message = str(cm.exception)
1208 self.assertEqual(MISSING_EVENT_TAG_OR_ATTRIBUTE, message)
1211class 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>"""
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)
1232 exporter = MockRedcapTaskExporter()
1233 project = exporter.get_project()
1235 project.is_longitudinal = mock.Mock(return_value=True)
1237 with self.assertRaises(RedcapExportException) as cm:
1238 exporter.export_task(self.req, exported_task_redcap)
1240 message = str(cm.exception)
1241 self.assertEqual(MISSING_EVENT_TAG_OR_ATTRIBUTE, message)
1244class AnonymousTaskRedcapTests(RedcapExportTestCase):
1245 def setUp(self) -> None:
1246 super().setUp()
1248 self.task = APEQCPFTPerinatalFactory()
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)
1254 exporter = MockRedcapTaskExporter()
1256 with self.assertRaises(RedcapExportException) as cm:
1257 exporter.export_task(self.req, exported_task_redcap)
1259 message = str(cm.exception)
1260 self.assertIn("Skipping anonymous task 'apeq_cpft_perinatal'", message)