Skip to content

Commit 293de56

Browse files
authored
Creates an incident when a case is moved to the escalated status (Netflix#2482)
* Creates an incident when a case is moved to the escalated status * Fixes and improvements * Removes dupe timeline event * Incident description and case tactical group update * Fixes and improvements * We add the incident's tactical group to the case's storage folder instead * Fixes number of arguments in function call * Changes default role for drive members from owner to writer
1 parent e98199f commit 293de56

File tree

7 files changed

+268
-99
lines changed

7 files changed

+268
-99
lines changed

src/dispatch/case/flows.py

Lines changed: 123 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
23
from datetime import datetime
34

45
from dispatch.case.models import CaseRead
@@ -9,7 +10,15 @@
910
from dispatch.event import service as event_service
1011
from dispatch.group import flows as group_flows
1112
from dispatch.group.enums import GroupType, GroupAction
13+
from dispatch.incident import flows as incident_flows
14+
from dispatch.incident import service as incident_service
15+
from dispatch.incident.enums import IncidentStatus
16+
from dispatch.incident.models import IncidentCreate
17+
from dispatch.individual.models import IndividualContactRead
18+
from dispatch.models import OrganizationSlug
19+
from dispatch.participant.models import ParticipantUpdate
1220
from dispatch.storage import flows as storage_flows
21+
from dispatch.storage.enums import StorageAction
1322
from dispatch.ticket import flows as ticket_flows
1423

1524
from .models import Case, CaseStatus
@@ -20,7 +29,7 @@
2029

2130

2231
@background_task
23-
def case_new_create_flow(*, case_id: int, organization_slug: str, db_session=None):
32+
def case_new_create_flow(*, case_id: int, organization_slug: OrganizationSlug, db_session=None):
2433
"""Runs the case new creation flow."""
2534
# we get the case
2635
case = get(db_session=db_session, case_id=case_id)
@@ -53,8 +62,10 @@ def case_new_create_flow(*, case_id: int, organization_slug: str, db_session=Non
5362
return
5463

5564
# we create the storage folder
56-
members = [group.email]
57-
storage = storage_flows.create_storage(obj=case, members=members, db_session=db_session)
65+
storage_members = [group.email]
66+
storage = storage_flows.create_storage(
67+
obj=case, storage_members=storage_members, db_session=db_session
68+
)
5869
if not storage:
5970
# we delete the group
6071
group_flows.delete_group(group=group, db_session=db_session)
@@ -97,14 +108,14 @@ def case_new_create_flow(*, case_id: int, organization_slug: str, db_session=Non
97108
document=document, project_id=case.project.id, db_session=db_session
98109
)
99110

100-
# we send the case created notification
111+
# TODO(mvilanova): we send the case created notification
101112

102113
db_session.add(case)
103114
db_session.commit()
104115

105116

106117
@background_task
107-
def case_triage_create_flow(*, case_id: int, organization_slug: str = None, db_session=None):
118+
def case_triage_create_flow(*, case_id: int, organization_slug: OrganizationSlug, db_session=None):
108119
"""Runs the case triage creation flow."""
109120
# we run the case new creation flow
110121
case_new_create_flow(
@@ -119,7 +130,9 @@ def case_triage_create_flow(*, case_id: int, organization_slug: str = None, db_s
119130

120131

121132
@background_task
122-
def case_escalated_create_flow(*, case_id: int, organization_slug: str = None, db_session=None):
133+
def case_escalated_create_flow(
134+
*, case_id: int, organization_slug: OrganizationSlug, db_session=None
135+
):
123136
"""Runs the case escalated creation flow."""
124137
# we run the case new creation flow
125138
case_new_create_flow(
@@ -133,11 +146,13 @@ def case_escalated_create_flow(*, case_id: int, organization_slug: str = None, d
133146
case_triage_status_flow(case=case, db_session=db_session)
134147

135148
# we transition the case to the escalated state
136-
case_escalated_status_flow(case=case, db_session=db_session)
149+
case_escalated_status_flow(
150+
case=case, organization_slug=organization_slug, db_session=db_session
151+
)
137152

138153

139154
@background_task
140-
def case_closed_create_flow(*, case_id: int, organization_slug: str = None, db_session=None):
155+
def case_closed_create_flow(*, case_id: int, organization_slug: OrganizationSlug, db_session=None):
141156
"""Runs the case closed creation flow."""
142157
# we run the case new creation flow
143158
case_new_create_flow(
@@ -160,7 +175,7 @@ def case_update_flow(
160175
case_id: int,
161176
previous_case: CaseRead,
162177
user_email: str,
163-
organization_slug: str = None,
178+
organization_slug: OrganizationSlug,
164179
db_session=None,
165180
):
166181
"""Runs the case update flow."""
@@ -169,15 +184,25 @@ def case_update_flow(
169184

170185
# we run the transition flow based on the current and previous status of the case
171186
case_status_transition_flow_dispatcher(
172-
case, case.status, previous_case.status, db_session=db_session
187+
case=case,
188+
current_status=case.status,
189+
previous_status=previous_case.status,
190+
organization_slug=organization_slug,
191+
db_session=db_session,
173192
)
174193

194+
# TODO(mvilanova): check if ticket has changed
175195
# we update the ticket
176196
ticket_flows.update_case_ticket(case=case, db_session=db_session)
177197

198+
# TODO(mvilanova): check if assignee has changed
178199
# we update the group membership
179200
group_flows.update_group(
180-
group=case.tactical_group, group_action=GroupAction.add_member, db_session=db_session
201+
obj=case,
202+
group=case.tactical_group,
203+
group_action=GroupAction.add_member,
204+
group_member=case.assignee.email,
205+
db_session=db_session,
181206
)
182207

183208
# we send the case updated notification
@@ -199,23 +224,9 @@ def case_delete_flow(case: Case, db_session: SessionLocal):
199224
storage_flows.delete_storage(storage=case.storage, db_session=db_session)
200225

201226

202-
def case_status_flow_common(case: Case, db_session=None):
203-
"""Runs tasks common across case status transition flows."""
204-
# we update the ticket
205-
ticket_flows.update_case_ticket(case=case, db_session=db_session)
206-
207-
# we update the timeline
208-
event_service.log_case_event(
209-
db_session=db_session,
210-
source="Dispatch Core App",
211-
description=f"The case status has been changed to {case.status.lower()}",
212-
case_id=case.id,
213-
)
214-
215-
216227
def case_new_status_flow(case: Case, db_session=None):
217228
"""Runs the case new transition flow."""
218-
case_status_flow_common(case=case, db_session=db_session)
229+
pass
219230

220231

221232
def case_triage_status_flow(case: Case, db_session=None):
@@ -225,17 +236,17 @@ def case_triage_status_flow(case: Case, db_session=None):
225236
db_session.add(case)
226237
db_session.commit()
227238

228-
case_status_flow_common(case=case, db_session=db_session)
229-
230239

231-
def case_escalated_status_flow(case: Case, db_session=None):
240+
def case_escalated_status_flow(case: Case, organization_slug: OrganizationSlug, db_session=None):
232241
"""Runs the case escalated transition flow."""
233242
# we set the escalated_at time
234243
case.escalated_at = datetime.utcnow()
235244
db_session.add(case)
236245
db_session.commit()
237246

238-
case_status_flow_common(case=case, db_session=db_session)
247+
case_to_incident_escalate_flow(
248+
case=case, organization_slug=organization_slug, db_session=db_session
249+
)
239250

240251

241252
def case_closed_status_flow(case: Case, db_session=None):
@@ -245,14 +256,13 @@ def case_closed_status_flow(case: Case, db_session=None):
245256
db_session.add(case)
246257
db_session.commit()
247258

248-
case_status_flow_common(case=case, db_session=db_session)
249-
250259

251260
def case_status_transition_flow_dispatcher(
252261
case: Case,
253262
current_status: CaseStatus,
254263
previous_status: CaseStatus,
255-
db_session=SessionLocal,
264+
organization_slug: OrganizationSlug,
265+
db_session: SessionLocal,
256266
):
257267
"""Runs the correct flows based on the current and previous status of the case."""
258268
# we changed the status of the case to new
@@ -271,7 +281,7 @@ def case_status_transition_flow_dispatcher(
271281
elif current_status == CaseStatus.triage:
272282
if previous_status == CaseStatus.new:
273283
# New -> Triage
274-
case_triage_status_flow(case, db_session)
284+
case_triage_status_flow(case=case, db_session=db_session)
275285
elif previous_status == CaseStatus.escalated:
276286
# Escalated -> Triage
277287
pass
@@ -283,11 +293,15 @@ def case_status_transition_flow_dispatcher(
283293
elif current_status == CaseStatus.escalated:
284294
if previous_status == CaseStatus.new:
285295
# New -> Escalated
286-
case_triage_status_flow(case, db_session)
287-
case_escalated_status_flow(case, db_session)
296+
case_triage_status_flow(case=case, db_session=db_session)
297+
case_escalated_status_flow(
298+
case=case, organization_slug=organization_slug, db_session=db_session
299+
)
288300
elif previous_status == CaseStatus.triage:
289301
# Triage -> Escalated
290-
case_escalated_status_flow(case, db_session)
302+
case_escalated_status_flow(
303+
case=case, organization_slug=organization_slug, db_session=db_session
304+
)
291305
elif previous_status == CaseStatus.closed:
292306
# Closed -> Escalated
293307
pass
@@ -296,10 +310,79 @@ def case_status_transition_flow_dispatcher(
296310
elif current_status == CaseStatus.closed:
297311
if previous_status == CaseStatus.new:
298312
# New -> Closed
299-
case_closed_status_flow(case, db_session)
313+
case_triage_status_flow(case=case, db_session=db_session)
314+
case_closed_status_flow(case=case, db_session=db_session)
300315
elif previous_status == CaseStatus.triage:
301316
# Triage -> Closed
302-
case_closed_status_flow(case, db_session)
317+
case_closed_status_flow(case=case, db_session=db_session)
303318
elif previous_status == CaseStatus.escalated:
304319
# Escalated -> Closed
305-
case_closed_status_flow(case, db_session)
320+
case_closed_status_flow(case=case, db_session=db_session)
321+
322+
323+
def case_to_incident_escalate_flow(
324+
case: Case, organization_slug: OrganizationSlug, db_session=None
325+
):
326+
"""Escalates a case to an incident if the case's type is mapped to an incident type."""
327+
if case.incidents:
328+
# we don't escalate the case if the case is already linked to incidents
329+
return
330+
331+
if not case.case_type.incident_type:
332+
# we don't escalate the case if its type is not mapped to an incident type
333+
return
334+
335+
# we make the assignee of the case the reporter of the incident
336+
reporter = ParticipantUpdate(individual=IndividualContactRead(email=case.assignee.email))
337+
338+
# we add information about the case in the incident's description
339+
description = (
340+
f"{case.description}\n\n"
341+
f"This incident was the result of escalating case {case.name} "
342+
f"in the {case.project.name} project. Check out the case in the Dispatch Web UI for additional context."
343+
)
344+
345+
# we create the incident
346+
incident_in = IncidentCreate(
347+
title=case.title,
348+
description=description,
349+
status=IncidentStatus.active,
350+
incident_type=case.case_type.incident_type,
351+
incident_priority=case.case_priority,
352+
visibility=case.visibility,
353+
project=case.case_type.incident_type.project,
354+
reporter=reporter,
355+
)
356+
incident = incident_service.create(db_session=db_session, incident_in=incident_in)
357+
358+
# we map the case to the newly created incident
359+
case.incidents.append(incident)
360+
361+
# we run the incident creation flow
362+
incident_flows.incident_create_flow(
363+
incident_id=incident.id, organization_slug=organization_slug, db_session=db_session
364+
)
365+
366+
event_service.log_case_event(
367+
db_session=db_session,
368+
source="Dispatch Core App",
369+
description=f"The case has been linked to incident {incident.name} in the {incident.project.name} project",
370+
case_id=case.id,
371+
)
372+
373+
# we add the incident's tactical group to the case's storage folder
374+
# to allow incident participants to access the case's artifacts in the folder
375+
storage_members = [incident.tactical_group.email]
376+
storage_flows.update_storage(
377+
obj=case,
378+
storage_action=StorageAction.add_members,
379+
storage_members=storage_members,
380+
db_session=db_session,
381+
)
382+
383+
event_service.log_case_event(
384+
db_session=db_session,
385+
source="Dispatch Core App",
386+
description=f"The members of the incident's tactical group {incident.tactical_group.email} have been given permission to access the case's storage folder",
387+
case_id=case.id,
388+
)

src/dispatch/group/flows.py

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def create_group(
7070
description="Case group created",
7171
case_id=obj.id,
7272
)
73-
else:
73+
if obj_type == "incident":
7474
event_service.log_incident_event(
7575
db_session=db_session,
7676
source=plugin.plugin.title,
@@ -81,36 +81,55 @@ def create_group(
8181
return group
8282

8383

84-
def update_group(group: Group, group_action: GroupAction, db_session: SessionLocal):
84+
def update_group(
85+
obj: Any, group: Group, group_action: GroupAction, group_member: str, db_session: SessionLocal
86+
):
8587
"""Updates an existing group."""
8688
plugin = plugin_service.get_active_instance(
87-
db_session=db_session, project_id=group.case.project.id, plugin_type="participant-group"
89+
db_session=db_session, project_id=obj.project.id, plugin_type="participant-group"
8890
)
89-
if plugin:
90-
# we check if the assignee is a member of the group
91+
if not plugin:
92+
log.warning("Group not updated. No group plugin enabled.")
93+
return
94+
95+
# we get the list of group members
96+
try:
97+
group_members = plugin.instance.list(email=group.email)
98+
except Exception as e:
99+
log.exception(e)
100+
return
101+
102+
# we add the member to the group if it's not a member
103+
if group_action == GroupAction.add_member and group_member not in group_members:
91104
try:
92-
group_members = plugin.instance.list(email=group.email)
105+
plugin.instance.add(email=group.email, participants=[group_member])
93106
except Exception as e:
94107
log.exception(e)
108+
return
95109

96-
if (
97-
group_action == GroupAction.add_member
98-
and group.case.assignee.email not in group_members
99-
):
100-
# we only try to add the user to the group if it's not a member
101-
try:
102-
plugin.instance.add(email=group.email, participants=[group.case.assignee.email])
103-
except Exception as e:
104-
log.exception(e)
105-
106-
if group_action == GroupAction.remove_member and group.case.assignee.email in group_members:
107-
# we only try to remove the user from the group if it's a member
108-
try:
109-
plugin.instance.remove(email=group.email, participants=[group.case.assignee.email])
110-
except Exception as e:
111-
log.exception(e)
112-
else:
113-
log.warning("Group not updated. No group plugin enabled.")
110+
# we remove the member from the group if it's a member
111+
if group_action == GroupAction.remove_member and group_member in group_members:
112+
try:
113+
plugin.instance.remove(email=group.email, participants=[group_member])
114+
except Exception as e:
115+
log.exception(e)
116+
return
117+
118+
obj_type = get_table_name_by_class_instance(obj)
119+
if obj_type == "case":
120+
event_service.log_case_event(
121+
db_session=db_session,
122+
source=plugin.plugin.title,
123+
description="Case group updated",
124+
case_id=obj.id,
125+
)
126+
if obj_type == "incident":
127+
event_service.log_incident_event(
128+
db_session=db_session,
129+
source=plugin.plugin.title,
130+
description="Incident group updated",
131+
incident_id=obj.id,
132+
)
114133

115134

116135
def delete_group(group: Group, db_session: SessionLocal):

0 commit comments

Comments
 (0)