Skip to content

Commit 79e292d

Browse files
authored
Add events command to legal-hold (#264)
1 parent d35aa9a commit 79e292d

File tree

4 files changed

+184
-18
lines changed

4 files changed

+184
-18
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ how a consumer would use the library (e.g. adding unit tests, updating documenta
1010

1111
## 1.4.1 - 2021-04-15
1212

13+
### Added
14+
15+
- `code42 legal-hold search-events` command:
16+
- `--matter-id` filters based on a legal hold uid.
17+
- `--begin` filters based on a beginning timestamp.
18+
- `--end` filters based on an end timestamp.
19+
- `--event-type` filters based on a list of event types.
20+
21+
## 1.4.1 - 2021-04-15
22+
1323
### Fixed
1424

1525
- Arguments/options that read data from files now attempt to autodetect file encodings.

docs/userguides/legalhold.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,12 @@ To view all custodians (including inactive) for a legal hold matter, enter
9393

9494
`code42 legal-hold show <matterID> --include-inactive`
9595

96+
### List legal hold events
97+
98+
To view a list of legal hold administrative events, use the following command:
99+
100+
`code42 legal-hold search-events`
101+
102+
This command takes the optional filters of a specific matter uid, beginning timestamp, end timestamp, and event type.
103+
96104
Learn more about the [Legal Hold](../commands/legalhold.md) commands.

src/code42cli/cmds/legal_hold.py

Lines changed: 73 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import json
2-
from collections import OrderedDict
32
from functools import lru_cache
43
from pprint import pformat
54

@@ -14,33 +13,52 @@
1413
from code42cli.file_readers import read_csv_arg
1514
from code42cli.options import format_option
1615
from code42cli.options import sdk_options
16+
from code42cli.options import set_begin_default_dict
17+
from code42cli.options import set_end_default_dict
1718
from code42cli.output_formats import OutputFormat
1819
from code42cli.output_formats import OutputFormatter
1920
from code42cli.util import format_string_list_to_columns
2021

2122

22-
_MATTER_KEYS_MAP = OrderedDict()
23-
_MATTER_KEYS_MAP["legalHoldUid"] = "Matter ID"
24-
_MATTER_KEYS_MAP["name"] = "Name"
25-
_MATTER_KEYS_MAP["description"] = "Description"
26-
_MATTER_KEYS_MAP["creator_username"] = "Creator"
27-
_MATTER_KEYS_MAP["creationDate"] = "Creation Date"
23+
_MATTER_KEYS_MAP = {
24+
"legalHoldUid": "Matter ID",
25+
"name": "Name",
26+
"description": "Description",
27+
"creator_username": "Creator",
28+
"creationDate": "Creation Date",
29+
}
30+
_EVENT_KEYS_MAP = {
31+
"eventUid": "Event ID",
32+
"eventType": "Event Type",
33+
"eventDate": "Event Date",
34+
"legalHoldUid": "Legal Hold ID",
35+
"actorUsername": "Actor Username",
36+
"custodianUsername": "Custodian Username",
37+
}
38+
LEGAL_HOLD_KEYWORD = "legal hold events"
39+
LEGAL_HOLD_EVENT_TYPES = [
40+
"MembershipCreated",
41+
"MembershipReactivated",
42+
"MembershipDeactivated",
43+
"HoldCreated",
44+
"HoldDeactivated",
45+
"HoldReactivated",
46+
"Restore",
47+
]
48+
BEGIN_DATE_DICT = set_begin_default_dict(LEGAL_HOLD_KEYWORD)
49+
END_DATE_DICT = set_end_default_dict(LEGAL_HOLD_KEYWORD)
2850

2951

3052
@click.group(cls=OrderedGroup)
3153
@sdk_options(hidden=True)
3254
def legal_hold(state):
3355
"""Add and remove custodians from legal hold matters."""
34-
pass
3556

3657

37-
matter_id_option = click.option(
38-
"-m",
39-
"--matter-id",
40-
required=True,
41-
type=str,
42-
help="Identification number of the legal hold matter the custodian will be added to.",
43-
)
58+
def matter_id_option(required, help):
59+
return click.option("-m", "--matter-id", required=required, type=str, help=help)
60+
61+
4462
user_id_option = click.option(
4563
"-u",
4664
"--username",
@@ -51,7 +69,10 @@ def legal_hold(state):
5169

5270

5371
@legal_hold.command()
54-
@matter_id_option
72+
@matter_id_option(
73+
True,
74+
"Identification number of the legal hold matter the custodian will be added to.",
75+
)
5576
@user_id_option
5677
@sdk_options()
5778
def add_user(state, matter_id, username):
@@ -60,7 +81,10 @@ def add_user(state, matter_id, username):
6081

6182

6283
@legal_hold.command()
63-
@matter_id_option
84+
@matter_id_option(
85+
True,
86+
"Identification number of the legal hold matter the custodian will be removed from.",
87+
)
6488
@user_id_option
6589
@sdk_options()
6690
def remove_user(state, matter_id, username):
@@ -124,6 +148,30 @@ def show(state, matter_id, include_inactive=False, include_policy=False):
124148
echo("")
125149

126150

151+
@legal_hold.command()
152+
@matter_id_option(False, "Filter results by legal hold UID.")
153+
@click.option(
154+
"--event-type",
155+
type=click.Choice(LEGAL_HOLD_EVENT_TYPES),
156+
help="Filter results by event types.",
157+
)
158+
@click.option("--begin", **BEGIN_DATE_DICT)
159+
@click.option("--end", **END_DATE_DICT)
160+
@format_option
161+
@sdk_options()
162+
def search_events(state, matter_id, event_type, begin, end, format):
163+
"""Tools for getting legal hold event data."""
164+
formatter = OutputFormatter(format, _EVENT_KEYS_MAP)
165+
events = _get_all_events(state.sdk, matter_id, begin, end)
166+
if event_type:
167+
events = [event for event in events if event["eventType"] == event_type]
168+
if len(events) > 10:
169+
output = formatter.get_formatted_output(events)
170+
click.echo_via_pager(output)
171+
else:
172+
formatter.echo_formatted_list(events)
173+
174+
127175
@legal_hold.group(cls=OrderedGroup)
128176
@sdk_options(hidden=True)
129177
def bulk(state):
@@ -230,6 +278,14 @@ def _get_all_active_matters(sdk):
230278
return matters
231279

232280

281+
def _get_all_events(sdk, legal_hold_uid, begin_date, end_date):
282+
events_generator = sdk.legalhold.get_all_events(
283+
legal_hold_uid, begin_date, end_date
284+
)
285+
events = [event for page in events_generator for event in page["legalHoldEvents"]]
286+
return events
287+
288+
233289
def _print_matter_members(username_list, member_type="active"):
234290
if username_list:
235291
echo("\n{} matter members:\n".format(member_type.capitalize()))

tests/cmds/test_legal_hold.py

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import datetime
2+
13
import pytest
24
from py42.exceptions import Py42BadRequestError
35
from py42.response import Py42Response
@@ -6,9 +8,9 @@
68

79
from code42cli import PRODUCT_NAME
810
from code42cli.cmds.legal_hold import _check_matter_is_accessible
11+
from code42cli.date_helper import convert_datetime_to_timestamp
912
from code42cli.main import cli
1013

11-
1214
_NAMESPACE = "{}.cmds.legal_hold".format(PRODUCT_NAME)
1315
TEST_MATTER_ID = "99999"
1416
TEST_LEGAL_HOLD_MEMBERSHIP_UID = "88888"
@@ -18,6 +20,8 @@
1820
INACTIVE_TEST_USERNAME = "[email protected]"
1921
INACTIVE_TEST_USER_ID = "54321"
2022
TEST_POLICY_UID = "66666"
23+
_CREATE_EVENT_ID = "564564654566"
24+
_MEMBERSHIP_EVENT_ID = "74533457745"
2125
TEST_PRESERVATION_POLICY_UID = "1010101010"
2226
MATTER_RESPONSE = """
2327
{
@@ -169,6 +173,42 @@
169173
]
170174
}
171175
"""
176+
TEST_EVENT_PAGE = {
177+
"legalHoldEvents": [
178+
{
179+
"eventUid": "564564654566",
180+
"eventType": "HoldCreated",
181+
"eventDate": "2015-05-16T15:07:44.820Z",
182+
"legalHoldUid": "88888",
183+
"actorUserUid": "12345",
184+
"actorUsername": "[email protected]",
185+
"actorFirstName": "john",
186+
"actorLastName": "doe",
187+
"actorUserExtRef": None,
188+
"actorEmail": "[email protected]",
189+
},
190+
{
191+
"eventUid": "74533457745",
192+
"eventType": "MembershipCreated",
193+
"eventDate": "2019-05-17T15:07:44.820Z",
194+
"legalHoldUid": "88888",
195+
"legalHoldMembershipUid": "645576514441664433",
196+
"custodianUserUid": "12345",
197+
"custodianUsername": "[email protected]",
198+
"custodianFirstName": "kim",
199+
"custodianLastName": "jones",
200+
"custodianUserExtRef": None,
201+
"custodianEmail": "[email protected]",
202+
"actorUserUid": "1234512345",
203+
"actorUsername": "[email protected]",
204+
"actorFirstName": "john",
205+
"actorLastName": "doe",
206+
"actorUserExtRef": None,
207+
"actorEmail": "[email protected]",
208+
},
209+
]
210+
}
211+
EMPTY_EVENTS_RESPONSE = """{"legalHoldEvents": []}"""
172212
EMPTY_MATTERS_RESPONSE = """{"legalHolds": []}"""
173213
ALL_MATTERS_RESPONSE = """{{"legalHolds": [{}]}}""".format(MATTER_RESPONSE)
174214
LEGAL_HOLD_COMMAND = "legal-hold"
@@ -212,6 +252,15 @@ def active_and_inactive_legal_hold_memberships_response(mocker):
212252
return [_create_py42_response(mocker, ALL_ACTIVE_AND_INACTIVE_CUSTODIANS_RESPONSE)]
213253

214254

255+
@pytest.fixture
256+
def empty_events_response(mocker):
257+
return _create_py42_response(mocker, EMPTY_EVENTS_RESPONSE)
258+
259+
260+
def events_list_generator():
261+
yield TEST_EVENT_PAGE
262+
263+
215264
@pytest.fixture
216265
def get_user_id_success(cli_state):
217266
cli_state.sdk.users.get_by_username.return_value = {
@@ -246,6 +295,11 @@ def check_matter_accessible_failure(cli_state, custom_error):
246295
)
247296

248297

298+
@pytest.fixture
299+
def get_all_events_success(cli_state):
300+
cli_state.sdk.legalhold.get_all_events.return_value = events_list_generator()
301+
302+
249303
@pytest.fixture
250304
def user_already_added_response(mocker):
251305
mock_response = mocker.MagicMock(spec=Response)
@@ -575,6 +629,44 @@ def test_list_with_csv_format_returns_no_response_when_response_is_empty(
575629
assert "Matter ID,Name,Description,Creator,Creation Date" not in result.output
576630

577631

632+
def test_search_events_shows_events_that_respect_type_filters(
633+
runner, cli_state, get_all_events_success
634+
):
635+
636+
result = runner.invoke(
637+
cli,
638+
["legal-hold", "search-events", "--event-type", "HoldCreated"],
639+
obj=cli_state,
640+
)
641+
642+
assert _CREATE_EVENT_ID in result.output
643+
assert _MEMBERSHIP_EVENT_ID not in result.output
644+
645+
646+
def test_search_events_with_csv_returns_no_events_when_response_is_empty(
647+
runner, cli_state, get_all_events_success, empty_events_response
648+
):
649+
cli_state.sdk.legalhold.get_all_events.return_value = empty_events_response
650+
result = runner.invoke(cli, ["legal-hold", "events", "-f", "csv"], obj=cli_state)
651+
652+
assert (
653+
"actorEmail,actorUsername,actorLastName,actorUserUid,actorUserExtRef"
654+
not in result.output
655+
)
656+
657+
658+
def test_search_events_is_called_with_expected_begin_timestamp(runner, cli_state):
659+
expected_timestamp = convert_datetime_to_timestamp(
660+
datetime.datetime.strptime("2017-01-01", "%Y-%m-%d")
661+
)
662+
command = ["legal-hold", "search-events", "--begin", "2017-01-01T00:00:00"]
663+
runner.invoke(cli, command, obj=cli_state)
664+
665+
cli_state.sdk.legalhold.get_all_events.assert_called_once_with(
666+
None, expected_timestamp, None
667+
)
668+
669+
578670
@pytest.mark.parametrize(
579671
"command, error_msg",
580672
[

0 commit comments

Comments
 (0)