Skip to content

Commit b2d1f44

Browse files
Update user (#300)
1 parent 1e606b9 commit b2d1f44

File tree

6 files changed

+256
-2
lines changed

6 files changed

+256
-2
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ how a consumer would use the library (e.g. adding unit tests, updating documenta
1010

1111
## Unreleased
1212

13+
### Added
14+
15+
- New command `code42 users update` to update a single user.
16+
17+
- New command `code42 users bulk update` to update users in bulk.
18+
1319
### Changed
1420

1521
- `code42 alerts search` now uses microsecond precision when searching by alerts' date observed.

src/code42cli/click_ext/groups.py

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from py42.exceptions import Py42LegalHoldNotFoundOrPermissionDeniedError
1313
from py42.exceptions import Py42UpdateClosedCaseError
1414
from py42.exceptions import Py42UserAlreadyAddedError
15+
from py42.exceptions import Py42UsernameMustBeEmailError
1516
from py42.exceptions import Py42UserNotOnListError
1617

1718
from code42cli.errors import Code42CLIError
@@ -67,6 +68,7 @@ def invoke(self, ctx):
6768
Py42DescriptionLimitExceededError,
6869
Py42CaseAlreadyHasEventError,
6970
Py42UpdateClosedCaseError,
71+
Py42UsernameMustBeEmailError,
7072
) as err:
7173
self.logger.log_error(err)
7274
raise Code42CLIError(str(err))

src/code42cli/cmds/devices.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,7 @@ def bulk(state):
573573
def bulk_deactivate(state, csv_rows, change_device_name, purge_date, format):
574574
"""Deactivate all devices from the provided CSV containing a 'guid' column."""
575575
sdk = state.sdk
576-
csv_rows[0]["deactivated"] = False
576+
csv_rows[0]["deactivated"] = "False"
577577
formatter = OutputFormatter(format, {key: key for key in csv_rows[0].keys()})
578578
for row in csv_rows:
579579
row["change_device_name"] = change_device_name

src/code42cli/cmds/users.py

+121
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import click
22
from pandas import DataFrame
33

4+
from code42cli.bulk import generate_template_cmd_factory
5+
from code42cli.bulk import run_bulk_process
46
from code42cli.click_ext.groups import OrderedGroup
57
from code42cli.click_ext.options import incompatible_with
68
from code42cli.errors import Code42CLIError
79
from code42cli.errors import UserDoesNotExistError
10+
from code42cli.file_readers import read_csv_arg
811
from code42cli.options import format_option
912
from code42cli.options import sdk_options
1013
from code42cli.output_formats import DataFrameOutputFormatter
1114
from code42cli.output_formats import OutputFormat
15+
from code42cli.output_formats import OutputFormatter
1216

1317

1418
@click.group(cls=OrderedGroup)
@@ -33,6 +37,11 @@ def users(state):
3337
)
3438

3539

40+
user_uid_option = click.option(
41+
"--user-id", help="The unique identifier of the user to be modified.", required=True
42+
)
43+
44+
3645
def role_name_option(help):
3746
return click.option("--role-name", help=help)
3847

@@ -84,6 +93,95 @@ def remove_role(state, username, role_name):
8493
_remove_user_role(state.sdk, role_name, username)
8594

8695

96+
@users.command(name="update")
97+
@user_uid_option
98+
@click.option("--username", help="The new username for the user.")
99+
@click.option("--password", help="The new password for the user.")
100+
@click.option("--email", help="The new email for the user.")
101+
@click.option("--first-name", help="The new first name for the user.")
102+
@click.option("--last-name", help="The new last name for the user.")
103+
@click.option("--notes", help="Notes about this user.")
104+
@click.option(
105+
"--archive-size-quota", help="The total size (in bytes) allowed for this user."
106+
)
107+
@sdk_options()
108+
def update_user(
109+
state,
110+
user_id,
111+
username,
112+
email,
113+
password,
114+
first_name,
115+
last_name,
116+
notes,
117+
archive_size_quota,
118+
):
119+
"""Update a user with the specified unique identifier."""
120+
_update_user(
121+
state.sdk,
122+
user_id,
123+
username,
124+
email,
125+
password,
126+
first_name,
127+
last_name,
128+
notes,
129+
archive_size_quota,
130+
)
131+
132+
133+
_bulk_user_update_headers = [
134+
"user_id",
135+
"username",
136+
"email",
137+
"password",
138+
"first_name",
139+
"last_name",
140+
"notes",
141+
"archive_size_quota",
142+
]
143+
144+
145+
@users.group(cls=OrderedGroup)
146+
@sdk_options(hidden=True)
147+
def bulk(state):
148+
"""Tools for managing users in bulk"""
149+
pass
150+
151+
152+
users_generate_template = generate_template_cmd_factory(
153+
group_name="users",
154+
commands_dict={"update": _bulk_user_update_headers},
155+
help_message="Generate the CSV template needed for bulk user commands.",
156+
)
157+
bulk.add_command(users_generate_template)
158+
159+
160+
@bulk.command(name="update")
161+
@read_csv_arg(headers=_bulk_user_update_headers)
162+
@format_option
163+
@sdk_options()
164+
def bulk_update(state, csv_rows, format):
165+
"""Update a list of users from the provided CSV."""
166+
csv_rows[0]["updated"] = "False"
167+
formatter = OutputFormatter(format, {key: key for key in csv_rows[0].keys()})
168+
169+
def handle_row(**row):
170+
try:
171+
_update_user(
172+
state.sdk, **{key: row[key] for key in row.keys() if key != "updated"}
173+
)
174+
row["updated"] = "True"
175+
except Exception as err:
176+
row["updated"] = f"False: {err}"
177+
return row
178+
179+
result_rows = run_bulk_process(
180+
handle_row, csv_rows, progress_label="Updating users:"
181+
)
182+
formatter.echo_formatted_list(result_rows)
183+
184+
87185
def _add_user_role(sdk, username, role_name):
88186
user_id = _get_user_id(sdk, username)
89187
_get_role_id(sdk, role_name) # function provides role name validation
@@ -122,3 +220,26 @@ def _get_users_dataframe(sdk, columns, org_uid, role_id, active):
122220
users_list.extend(page["users"])
123221

124222
return DataFrame.from_records(users_list, columns=columns)
223+
224+
225+
def _update_user(
226+
sdk,
227+
user_id,
228+
username,
229+
email,
230+
password,
231+
first_name,
232+
last_name,
233+
notes,
234+
archive_size_quota,
235+
):
236+
return sdk.users.update_user(
237+
user_id,
238+
username=username,
239+
email=email,
240+
password=password,
241+
first_name=first_name,
242+
last_name=last_name,
243+
notes=notes,
244+
archive_size_quota_bytes=archive_size_quota,
245+
)

tests/cmds/test_devices.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -822,7 +822,7 @@ def test_bulk_deactivate_uses_expected_arguments(runner, mocker, cli_state):
822822
assert bulk_processor.call_args[0][1] == [
823823
{
824824
"guid": "test",
825-
"deactivated": False,
825+
"deactivated": "False",
826826
"change_device_name": False,
827827
"purge_date": None,
828828
}

tests/cmds/test_users.py

+125
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from code42cli.main import cli
88

99

10+
_NAMESPACE = "code42cli.cmds.users"
11+
1012
TEST_ROLE_RETURN_DATA = {
1113
"data": [{"roleName": "Customer Cloud Admin", "roleId": "1234543"}]
1214
}
@@ -50,6 +52,11 @@ def get_all_users_generator():
5052
yield TEST_USERS_RESPONSE
5153

5254

55+
@pytest.fixture
56+
def update_user_response(mocker):
57+
return _create_py42_response(mocker, "")
58+
59+
5360
@pytest.fixture
5461
def get_available_roles_response(mocker):
5562
return _create_py42_response(mocker, json.dumps(TEST_ROLE_RETURN_DATA))
@@ -75,6 +82,11 @@ def get_available_roles_success(cli_state, get_available_roles_response):
7582
cli_state.sdk.users.get_available_roles.return_value = get_available_roles_response
7683

7784

85+
@pytest.fixture
86+
def update_user_success(cli_state, update_user_response):
87+
cli_state.sdk.users.update_user.return_value = update_user_response
88+
89+
7890
def test_list_when_non_table_format_outputs_expected_columns(
7991
runner, cli_state, get_all_users_success
8092
):
@@ -263,3 +275,116 @@ def test_remove_user_role_raises_error_when_username_does_not_exist(
263275
result = runner.invoke(cli, command, obj=cli_state)
264276
assert result.exit_code == 1
265277
assert "User '[email protected]' does not exist." in result.output
278+
279+
280+
def test_update_user_calls_update_user_with_correct_parameters_when_only_some_are_passed(
281+
runner, cli_state, update_user_success
282+
):
283+
command = ["users", "update", "--user-id", "12345", "--email", "test_email"]
284+
runner.invoke(cli, command, obj=cli_state)
285+
cli_state.sdk.users.update_user.assert_called_once_with(
286+
"12345",
287+
username=None,
288+
email="test_email",
289+
password=None,
290+
first_name=None,
291+
last_name=None,
292+
notes=None,
293+
archive_size_quota_bytes=None,
294+
)
295+
296+
297+
def test_update_user_calls_update_user_with_correct_parameters_when_all_are_passed(
298+
runner, cli_state, update_user_success
299+
):
300+
command = [
301+
"users",
302+
"update",
303+
"--user-id",
304+
"12345",
305+
"--email",
306+
"test_email",
307+
"--username",
308+
"test_username",
309+
"--password",
310+
"test_password",
311+
"--first-name",
312+
"test_fname",
313+
"--last-name",
314+
"test_lname",
315+
"--notes",
316+
"test notes",
317+
"--archive-size-quota",
318+
"123456",
319+
]
320+
runner.invoke(cli, command, obj=cli_state)
321+
cli_state.sdk.users.update_user.assert_called_once_with(
322+
"12345",
323+
username="test_username",
324+
email="test_email",
325+
password="test_password",
326+
first_name="test_fname",
327+
last_name="test_lname",
328+
notes="test notes",
329+
archive_size_quota_bytes="123456",
330+
)
331+
332+
333+
def test_bulk_deactivate_uses_expected_arguments_when_only_some_are_passed(
334+
runner, mocker, cli_state
335+
):
336+
bulk_processor = mocker.patch(f"{_NAMESPACE}.run_bulk_process")
337+
with runner.isolated_filesystem():
338+
with open("test_bulk_update.csv", "w") as csv:
339+
csv.writelines(
340+
[
341+
"user_id,username,email,password,first_name,last_name,notes,archive_size_quota\n",
342+
"12345,,test_email,,,,,\n",
343+
]
344+
)
345+
runner.invoke(
346+
cli, ["users", "bulk", "update", "test_bulk_update.csv"], obj=cli_state
347+
)
348+
assert bulk_processor.call_args[0][1] == [
349+
{
350+
"user_id": "12345",
351+
"username": "",
352+
"email": "test_email",
353+
"password": "",
354+
"first_name": "",
355+
"last_name": "",
356+
"notes": "",
357+
"archive_size_quota": "",
358+
"updated": "False",
359+
}
360+
]
361+
362+
363+
def test_bulk_deactivate_uses_expected_arguments_when_all_are_passed(
364+
runner, mocker, cli_state
365+
):
366+
bulk_processor = mocker.patch(f"{_NAMESPACE}.run_bulk_process")
367+
with runner.isolated_filesystem():
368+
with open("test_bulk_update.csv", "w") as csv:
369+
csv.writelines(
370+
[
371+
"user_id,username,email,password,first_name,last_name,notes,archive_size_quota\n",
372+
"12345,test_username,test_email,test_pword,test_fname,test_lname,test notes,4321\n",
373+
]
374+
)
375+
runner.invoke(
376+
cli, ["users", "bulk", "update", "test_bulk_update.csv"], obj=cli_state
377+
)
378+
assert bulk_processor.call_args[0][1] == [
379+
{
380+
"user_id": "12345",
381+
"username": "test_username",
382+
"email": "test_email",
383+
"password": "test_pword",
384+
"first_name": "test_fname",
385+
"last_name": "test_lname",
386+
"notes": "test notes",
387+
"archive_size_quota": "4321",
388+
"updated": "False",
389+
}
390+
]

0 commit comments

Comments
 (0)