diff --git a/README.md b/README.md index e903bb5..9f58f6e 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ Currently, these API calls are available: * File * Records * Users +* User Roles ### Other diff --git a/redcap/conftest.py b/redcap/conftest.py index b32bccd..8da7dd8 100644 --- a/redcap/conftest.py +++ b/redcap/conftest.py @@ -8,7 +8,11 @@ import pytest from redcap.project import Project -from tests.integration.conftest import create_project, SUPER_TOKEN +from tests.integration.conftest import ( + create_project, + grant_superuser_rights, + SUPER_TOKEN, +) @pytest.fixture(scope="session", autouse=True) @@ -22,5 +26,6 @@ def add_doctest_objects(doctest_namespace): project_xml_path=doctest_project_xml, ) doctest_project = Project(url, doctest_token) + doctest_project = grant_superuser_rights(doctest_project) doctest_namespace["proj"] = doctest_project doctest_namespace["TOKEN"] = doctest_token diff --git a/redcap/methods/data_access_groups.py b/redcap/methods/data_access_groups.py index 5175b47..9e4a92d 100644 --- a/redcap/methods/data_access_groups.py +++ b/redcap/methods/data_access_groups.py @@ -142,7 +142,7 @@ def delete_dags( Delete dags from the project. Args: - dags: List of usernames to delete from the project + dags: List of dags to delete from the project return_format_type: Response format. By default, response will be json-decoded. diff --git a/redcap/methods/user_roles.py b/redcap/methods/user_roles.py index dda285f..6c82497 100644 --- a/redcap/methods/user_roles.py +++ b/redcap/methods/user_roles.py @@ -47,7 +47,7 @@ def export_user_roles( Examples: >>> proj.export_user_roles() - [{'unique_role_name': ..., 'role_label': 'Test role', 'design': '0', 'user_rights': '0', + [{'unique_role_name': ..., 'role_label': 'New Role', 'design': '0', 'user_rights': '0', 'data_access_groups': '0', 'reports': '0', 'stats_and_charts': '0', 'manage_survey_participants': '0', 'calendar': '0', 'data_import_tool': '0', 'data_comparison_tool': '0', 'logging': '0', 'file_repository': '0', @@ -55,7 +55,7 @@ def export_user_roles( 'api_import': '0', 'mobile_app': '0', 'mobile_app_download_data': '0', 'record_create': '0', 'record_rename': '0', 'record_delete': '0', 'lock_records_customization': '0', 'lock_records': '0', ..., - 'forms': {'form_1': 2}, 'forms_export': {'form_1': 0}}] + 'forms': {'form_1': 0}, 'forms_export': {'form_1': 0}}] """ payload = self._initialize_payload(content="userRole", format_type=format_type) return_type = self._lookup_return_type(format_type, request_type="export") @@ -127,6 +127,63 @@ def import_user_roles( return response + @overload + def delete_user_roles( + self, user_roles: List[str], return_format_type: Literal["json"] + ) -> int: + ... + + @overload + def delete_user_roles( + self, user_roles: List[str], return_format_type: Literal["csv", "xml"] + ) -> str: + ... + + def delete_user_roles( + self, + roles: List[str], + return_format_type: Literal["json", "csv", "xml"] = "json", + ): + """ + Delete user roles from the project. + + Args: + roles: List of user roles to delete from the project + return_format_type: + Response format. By default, response will be json-decoded. + + Returns: + Union[int, str]: Number of user roles deleted + + Examples: + Create a new user role + >>> new_role = [{"role_label": "New Role"}] + >>> proj.import_user_roles(new_role) + 1 + + We don't know what the 'unique_role_name' is for the newly created role, + so we have to find that out. Since it's the last role created, it should + be the last one in the export result + >>> new_role_id = proj.export_user_roles()[-1]["unique_role_name"] + + Delete the role + >>> proj.delete_user_roles([new_role_id]) + 1 + """ + payload = self._initialize_payload( + content="userRole", return_format_type=return_format_type + ) + payload["action"] = "delete" + # Turn list of user roles into dict, and append to payload + roles_dict = {f"roles[{ idx }]": role for idx, role in enumerate(roles)} + payload.update(roles_dict) + + return_type = self._lookup_return_type( + format_type=return_format_type, request_type="delete" + ) + response = self._call_api(payload, return_type) + return response + @overload def export_user_role_assignment( self, format_type: Literal["json"], df_kwargs: None diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index df8c642..add9fe5 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -39,8 +39,9 @@ def create_project(url: str, super_token: str, project_xml_path: Path) -> str: "odm": project_data, }, ) - - return res.text + # Response includes a bunch of SQL statements before we get to the token + # This limits the return value to just the token + return res.text[-32:] @pytest.fixture(scope="module") @@ -58,10 +59,29 @@ def simple_project_token(redcapdemo_url) -> str: return project_token +def grant_superuser_rights(proj: Project) -> Project: + """Given a newly created project, give the superuser + the highest level of user rights + """ + superuser = proj.export_users()[0] + + superuser["record_delete"] = 1 + superuser["record_rename"] = 1 + superuser["lock_records_all_forms"] = 1 + superuser["lock_records"] = 1 + + res = proj.import_users([superuser]) + assert res == 1 + + return proj + + @pytest.fixture(scope="module") def simple_project(redcapdemo_url, simple_project_token): """A simple REDCap project""" - return Project(redcapdemo_url, simple_project_token) + simple_proj = Project(redcapdemo_url, simple_project_token) + simple_proj = grant_superuser_rights(simple_proj) + return simple_proj @pytest.fixture(scope="module") @@ -76,4 +96,6 @@ def long_project_token(redcapdemo_url) -> str: @pytest.fixture(scope="module") def long_project(redcapdemo_url, long_project_token): """A long REDCap project""" - return Project(redcapdemo_url, long_project_token) + long_proj = Project(redcapdemo_url, long_project_token) + long_proj = grant_superuser_rights(long_proj) + return long_proj diff --git a/tests/integration/test_simple_project.py b/tests/integration/test_simple_project.py index 36273d4..c1f3dc9 100644 --- a/tests/integration/test_simple_project.py +++ b/tests/integration/test_simple_project.py @@ -119,6 +119,19 @@ def test_export_user_roles(simple_project): assert user_roles[0]["role_label"] == "Example Role" +@pytest.mark.integration +def test_import_delete_user_roles(simple_project): + new_role = [{"role_label": "New Role"}] + + res = simple_project.import_user_roles(new_role) + assert res == 1 + + new_role_id = simple_project.export_user_roles()[-1]["unique_role_name"] + + res = simple_project.delete_user_roles([new_role_id]) + assert res == 1 + + @pytest.mark.integration def test_export_import_user_role_assignments(simple_project): new_user = "pandeharris@gmail.com" @@ -247,4 +260,3 @@ def test_export_logging(simple_project): logs = simple_project.export_logging(log_type="manage") first_log = logs.pop() assert "manage/design" in first_log["action"].lower() - assert "multi-language" in first_log["details"].lower() diff --git a/tests/unit/callback_utils.py b/tests/unit/callback_utils.py index 2d38507..67f09dc 100644 --- a/tests/unit/callback_utils.py +++ b/tests/unit/callback_utils.py @@ -526,6 +526,9 @@ def handle_user_role_request(**kwargs) -> MockResponse: # user import (JSON only) if "data" in str(data): resp = 1 + # user delete (csv only) + elif "delete" in str(data): + resp = 1 # user export (JSON only) else: resp = [ diff --git a/tests/unit/test_simple_project.py b/tests/unit/test_simple_project.py index 45f80d9..159960e 100644 --- a/tests/unit/test_simple_project.py +++ b/tests/unit/test_simple_project.py @@ -233,6 +233,11 @@ def test_user_role_import(simple_project): assert res == 1 +def test_user_role_delete(simple_project): + res = simple_project.delete_user_roles(["1000"]) + assert res == 1 + + def test_export_user_role_assignment(simple_project): res = simple_project.export_user_role_assignment() assert len(res) == 1