Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ Currently, these API calls are available:
* File
* Records
* Users
* User Roles

### Other

Expand Down
7 changes: 6 additions & 1 deletion redcap/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
2 changes: 1 addition & 1 deletion redcap/methods/data_access_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
61 changes: 59 additions & 2 deletions redcap/methods/user_roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ 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',
'data_quality_create': '0', 'data_quality_execute': '0', 'api_export': '0',
'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")
Expand Down Expand Up @@ -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
Expand Down
30 changes: 26 additions & 4 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Expand All @@ -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
14 changes: 13 additions & 1 deletion tests/integration/test_simple_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()
3 changes: 3 additions & 0 deletions tests/unit/callback_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
5 changes: 5 additions & 0 deletions tests/unit/test_simple_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down