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
128 changes: 127 additions & 1 deletion redcap/methods/users.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""REDCap API methods for Project users"""
from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, overload
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union, overload

from redcap.methods.base import Base, Json

Expand Down Expand Up @@ -74,3 +74,129 @@ def export_users(
format_type=format_type,
df_kwargs=df_kwargs,
)

@overload
def import_users(
self,
to_import: Union[str, List[Dict[str, Any]], "pd.DataFrame"],
return_format_type: Literal["json"],
import_format: Literal["json", "csv", "xml", "df"] = "json",
) -> int:
...

@overload
def import_users(
self,
to_import: Union[str, List[Dict[str, Any]], "pd.DataFrame"],
return_format_type: Literal["csv", "xml"],
import_format: Literal["json", "csv", "xml", "df"] = "json",
) -> str:
...

def import_users(
self,
to_import: Union[str, List[Dict[str, Any]], "pd.DataFrame"],
return_format_type: Literal["json", "csv", "xml"] = "json",
import_format: Literal["json", "csv", "xml", "df"] = "json",
):
"""
Import users/user rights into the REDCap Project

Args:
to_import: array of dicts, csv/xml string, `pandas.DataFrame`
Note:
If you pass a csv or xml string, you should use the
`import format` parameter appropriately.
return_format_type:
Response format. By default, response will be json-decoded.
import_format:
Format of incoming data. By default, to_import will be json-encoded

Returns:
Union[int, str]: Number of users added or updated

Examples:
Add test user. Only username is required
>>> test_user = [{"username": "pandeharris@gmail.com"}]
>>> proj.import_users(test_user)
1

All currently valid options for user rights
>>> test_user = [
... {"username": "pandeharris@gmail.com", "email": "pandeharris@gmail.com",
... "firstname": "REDCap Trial", "lastname": "User", "expiration": "",
... "data_access_group": "", "data_access_group_id": "", "design": 0,
... "user_rights": 0, "data_export": 2, "reports": 1, "stats_and_charts": 1,
... "manage_survey_participants": 1, "calendar": 1, "data_access_groups": 0,
... "data_import_tool": 0, "data_comparison_tool": 0, "logging": 0,
... "file_repository": 1, "data_quality_create": 0, "data_quality_execute": 0,
... "api_export": 0, "api_import": 0, "mobile_app": 0,
... "mobile_app_download_data": 0, "record_create": 1, "record_rename": 0,
... "record_delete": 0, "lock_records_all_forms": 0, "lock_records": 0,
... "lock_records_customization": 0, "forms": {"form_1": 3}}
... ]
>>> proj.import_users(test_user)
1
"""
payload = self._initialize_import_payload(
to_import=to_import,
import_format=import_format,
return_format_type=return_format_type,
content="user",
)

return_type = self._lookup_return_type(
format_type=return_format_type, request_type="import"
)
response = self._call_api(payload, return_type)

return response

@overload
def delete_users(
self, users: List[str], return_format_type: Literal["json"]
) -> int:
...

@overload
def delete_users(
self, users: List[str], return_format_type: Literal["csv", "xml"]
) -> str:
...

def delete_users(
self,
users: List[str],
return_format_type: Literal["json", "csv", "xml"] = "json",
):
"""
Delete users from the project.

Args:
users: List of usernames to delete from the project
return_format_type:
Response format. By default, response will be json-decoded.

Returns:
Union[int, str]: Number of users deleted

Examples:
>>> new_user = [{"username": "pandeharris@gmail.com"}]
>>> proj.import_users(new_user)
1
>>> proj.delete_users(["pandeharris@gmail.com"], return_format_type="xml")
'1'
"""
payload = self._initialize_payload(
content="user", return_format_type=return_format_type
)
payload["action"] = "delete"
# Turn list of users into dict, and append to payload
users_dict = {f"users[{ idx }]": user for idx, user in enumerate(users)}
payload.update(users_dict)

return_type = self._lookup_return_type(
format_type=return_format_type, request_type="delete"
)
response = self._call_api(payload, return_type)
return response
13 changes: 13 additions & 0 deletions tests/integration/test_long_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ def test_users_export(long_project):
assert data.index.name == "email"


@pytest.mark.integration
def test_users_import_and_delete(long_project):
test_user = "pandeharris@gmail.com"
test_user_json = [{"username": test_user}]
res = long_project.import_users(test_user_json, return_format_type="csv")

assert res == "1"

res = long_project.delete_users([test_user])

assert res == 1


@pytest.mark.integration
def test_records_export_labeled_headers(long_project):
data = long_project.export_records(format_type="csv", raw_or_label_headers="label")
Expand Down
36 changes: 23 additions & 13 deletions tests/unit/callback_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,20 +452,30 @@ def handle_long_project_reports_request(**kwargs) -> MockResponse:
def handle_user_request(**kwargs) -> MockResponse:
"""Handle user export"""
headers = kwargs["headers"]
resp = [
{
"firstname": "test",
"lastname": "test",
"email": "test",
"username": "test",
"expiration": "test",
"data_access_group": "test",
"data_export": "test",
"forms": "test",
}
]
data = kwargs["data"]
# user import (JSON only)
if "data" in str(data):
resp = json.dumps(1)
# user delete (csv only)
elif "delete" in str(data):
resp = "1"
# user export (JSON only)
else:
resp = [
{
"firstname": "test",
"lastname": "test",
"email": "test",
"username": "test",
"expiration": "test",
"data_access_group": "test",
"data_export": "test",
"forms": "test",
}
]
resp = json.dumps(resp)

return (201, headers, json.dumps(resp))
return (201, headers, resp)


# pylint: disable=unused-argument
Expand Down
15 changes: 15 additions & 0 deletions tests/unit/test_simple_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,21 @@ def test_user_export(simple_project):
assert key in user


def test_user_import(simple_project):
test_user = [{"username": "test@gmail.com"}]
res = simple_project.import_users(test_user)

assert res == 1


def test_user_delete(simple_project):
res = simple_project.delete_users(
[{"username": "test@gmail.com"}], return_format_type="csv"
)

assert res == "1"


def test_generate_next_record_name(simple_project):
next_name = simple_project.generate_next_record_name()

Expand Down