diff --git a/README.md b/README.md index aad8cf2..a6c8745 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,11 @@ Currently, these API calls are available: * Records * Repeating instruments and events * Report +* Survey link +* Survey access code * Survey participant list +* Survey queue link +* Survey return code * Users * User-DAG assignment * User Roles diff --git a/redcap/methods/records.py b/redcap/methods/records.py index 67679c8..5dd6664 100644 --- a/redcap/methods/records.py +++ b/redcap/methods/records.py @@ -330,13 +330,41 @@ def import_records( def delete_records( self, records: List[str], + arm: Optional[str] = None, + instrument: Optional[str] = None, + event: Optional[str] = None, + repeat_instance: Optional[int] = None, + delete_logging: bool = False, return_format_type: Literal["json", "csv", "xml"] = "json", ): + # pylint: disable=line-too-long """ Delete records from the project. Args: records: List of record IDs to delete from the project + arm: + the arm number of the arm in which the record(s) should be deleted. + (This can only be used if the project is longitudinal with more than one arm.) + NOTE: If the arm parameter is not provided, the specified records will be + deleted from all arms in which they exist. Whereas, if arm is provided, + they will only be deleted from the specified arm. + instrument: + the unique instrument name (column B in the Data Dictionary) of an + instrument (as a string) if you wish to delete the data for all fields + on the specified instrument for the records specified. + event: + the unique event name - only for longitudinal projects. NOTE: If + instrument is provided for a longitudinal project, the event parameter + is mandatory. + repeat_instance: + the repeating instance number for a repeating instrument or repeating event. + NOTE: If project has repeating instruments/events, it will remove only the + data for that repeating instance. + delete_logging: + provide a value of False ("keep logging") or True ("delete logging"). This + activity when deleting the record?" setting enabled by an administrator on + the Edit Project Settings page. The default value for PyCap is False return_format_type: Response format. By default, response will be json-decoded. @@ -352,11 +380,38 @@ def delete_records( {'count': 2} >>> proj.delete_records(["3", "4"]) 2 + >>> new_record = [ + ... {"record_id": 3, "redcap_event_name": "event_1_arm_1", "redcap_repeat_instance": 1, "field_1": 1,}, + ... {"record_id": 3, "redcap_event_name": "event_1_arm_1", "redcap_repeat_instance": 2, "field_1": 0,}, + ... ] + >>> proj.import_records(new_record) + {'count': 1} + >>> proj.delete_records(records=["3"], event="event_1_arm_1", repeat_instance=2) + 1 + >>> proj.export_records(records=["3"]) + [{'record_id': '3', 'redcap_event_name': 'event_1_arm_1', 'redcap_repeat_instrument': '', 'redcap_repeat_instance': 1, + 'field_1': '1', 'checkbox_field___1': '0', 'checkbox_field___2': '0', 'upload_field': '', 'form_1_complete': '0'}] + >>> proj.delete_records(records=["3"]) + 1 """ + # pylint: enable=line-too-long payload = self._initialize_payload( content="record", return_format_type=return_format_type ) payload["action"] = "delete" + if delete_logging: + payload["delete_logging"] = "1" + else: + payload["delete_logging"] = "0" + + if arm: + payload["arm"] = arm + if instrument: + payload["instrument"] = instrument + if event: + payload["event"] = event + if repeat_instance: + payload["repeat_instance"] = repeat_instance # Turn list of records into dict, and append to payload records_dict = { f"records[{ idx }]": record for idx, record in enumerate(records) diff --git a/tests/integration/test_long_project.py b/tests/integration/test_long_project.py index cbff629..e23cb39 100644 --- a/tests/integration/test_long_project.py +++ b/tests/integration/test_long_project.py @@ -139,6 +139,38 @@ def test_limit_export_records_forms_and_fields(long_project): assert complete_cols == ["baseline_data_complete"] +def test_delete_records_from_one_instrument_only(long_project): + # Add new record to test partial deletion + new_record = [ + { + "study_id": "3", + "redcap_event_name": "enrollment_arm_1", + "redcap_repeat_instrument": "", + "redcap_repeat_instance": "", + }, + { + "study_id": "3", + "redcap_event_name": "visit_1_arm_1", + "redcap_repeat_instrument": "", + "redcap_repeat_instance": "", + }, + ] + res = long_project.import_records(new_record) + assert res["count"] == 1 + + res = long_project.export_records(records=["3"]) + assert len(res) == 2 + + res = long_project.delete_records(records=["3"], event="visit_1_arm_1") + assert res == 1 + + res = long_project.export_records(records=["3"]) + assert len(res) == 1 + # restore project to original state pre-test + res = long_project.delete_records(["3"]) + assert res == 1 + + @pytest.mark.integration def test_arms_export(long_project): response = long_project.export_arms() diff --git a/tests/unit/callback_utils.py b/tests/unit/callback_utils.py index 3d4249d..39fdadc 100644 --- a/tests/unit/callback_utils.py +++ b/tests/unit/callback_utils.py @@ -506,6 +506,23 @@ def handle_simple_project_delete_records(data: dict) -> int: return resp +def handle_long_project_delete_records(data: dict) -> int: + """Given long project delete request, determine how many records were deleted""" + resp = 0 + assert data["arm"] + assert data["instrument"] + assert data["event"] + assert data["repeat_instance"] + assert data["repeat_instance"] + assert data["delete_logging"] + + for key in data: + if "records[" in key: + resp += 1 + + return resp + + def handle_simple_project_import_records(data: dict) -> dict: """Given simple project import request, determine response""" resp = {"count": 2} @@ -572,6 +589,10 @@ def handle_long_project_records_request(**kwargs) -> Any: # if the None value gets returned it means the test failed resp = None headers = kwargs["headers"] + if "delete" in data.get("action", "other"): + resp = handle_long_project_delete_records(data) + + return (201, headers, json.dumps(resp)) # data import if "returnContent" in data: resp = {"count": 1} diff --git a/tests/unit/test_long_project.py b/tests/unit/test_long_project.py index 656deeb..07e5f4c 100644 --- a/tests/unit/test_long_project.py +++ b/tests/unit/test_long_project.py @@ -149,6 +149,19 @@ def test_export_with_events(long_project): assert isinstance(record, dict) +def test_delete_records_from_event(long_project): + res = long_project.delete_records( + records=["1"], + arm="1", + instrument="form_1", + event="enrollment_arm_1", + repeat_instance=1, + delete_logging=True, + ) + + assert res == 1 + + def test_fem_export(long_project): fem = long_project.export_instrument_event_mappings(format_type="json")