Skip to content

Commit 6bb06d4

Browse files
author
Will Sheldon
authored
[Enhancement] - Cases, but they can be Slack channels (Netflix#4551)
* Making cases, channels * Remove all the dupe code, move conditional closer to plugin usage * Add escalation messages * Add escalation messages * Latest, adds slash commands, better escalate flow, bookmarks, and more * Latest changes, slack refactor for commands, error handle messages, and ui updates * Remove unused NoReturn import * Bulk participant announce, rename instantly, modal updates * Remove debugging line * Update comment in alembic upgrade * Remove dead function * Fix revision * fix revision
1 parent eb14399 commit 6bb06d4

File tree

28 files changed

+1038
-345
lines changed

28 files changed

+1038
-345
lines changed

src/dispatch/case/flows.py

Lines changed: 182 additions & 114 deletions
Large diffs are not rendered by default.

src/dispatch/case/models.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from pydantic import validator
66
from sqlalchemy import (
7+
Boolean,
78
Column,
89
DateTime,
910
ForeignKey,
@@ -88,6 +89,8 @@ class Case(Base, TimeStampMixin, ProjectMixin):
8889
escalated_at = Column(DateTime)
8990
closed_at = Column(DateTime)
9091

92+
dedicated_channel = Column(Boolean, default=False)
93+
9194
search_vector = Column(
9295
TSVectorType(
9396
"name", "title", "description", weights={"name": "A", "title": "B", "description": "C"}
@@ -169,6 +172,14 @@ def participant_observer(self, participants):
169172
self.participants_team = Counter(p.team for p in participants).most_common(1)[0][0]
170173
self.participants_location = Counter(p.location for p in participants).most_common(1)[0][0]
171174

175+
@property
176+
def has_channel(self) -> bool:
177+
return True if not self.conversation.thread_id else False
178+
179+
@property
180+
def has_thread(self) -> bool:
181+
return True if self.conversation.thread_id else False
182+
172183

173184
class SignalRead(DispatchBase):
174185
id: PrimaryKey
@@ -222,6 +233,7 @@ class CaseCreate(CaseBase):
222233
case_priority: Optional[CasePriorityCreate]
223234
case_severity: Optional[CaseSeverityCreate]
224235
case_type: Optional[CaseTypeCreate]
236+
dedicated_channel: Optional[bool]
225237
project: Optional[ProjectRead]
226238
reporter: Optional[ParticipantUpdate]
227239
tags: Optional[List[TagRead]] = []

src/dispatch/case/service.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ def create(*, db_session, case_in: CaseCreate, current_user: DispatchUser = None
142142
description=case_in.description,
143143
project=project,
144144
status=case_in.status,
145+
dedicated_channel=case_in.dedicated_channel,
145146
tags=tag_objs,
146147
)
147148

src/dispatch/conversation/flows.py

Lines changed: 51 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,33 @@
11
import logging
22

3-
from typing import TypeVar, List
3+
from sqlalchemy.orm import Session
44

55
from dispatch.case.models import Case
66
from dispatch.conference.models import Conference
7-
from dispatch.database.core import SessionLocal, resolve_attr
7+
from dispatch.database.core import SessionLocal
88
from dispatch.document.models import Document
99
from dispatch.event import service as event_service
1010
from dispatch.incident.models import Incident
1111
from dispatch.plugin import service as plugin_service
1212
from dispatch.storage.models import Storage
1313
from dispatch.ticket.models import Ticket
1414
from dispatch.utils import deslug_and_capitalize_resource_type
15-
from dispatch.config import DISPATCH_UI_URL
15+
from dispatch.types import Subject
1616

1717
from .models import Conversation, ConversationCreate
1818
from .service import create
1919

2020
log = logging.getLogger(__name__)
2121

2222

23-
Resource = TypeVar("Resource", Document, Conference, Storage, Ticket)
23+
Resource = Document | Conference | Storage | Ticket
2424

2525

26-
def create_case_conversation(case: Case, conversation_target: str, db_session: SessionLocal):
26+
def create_case_conversation(
27+
case: Case,
28+
conversation_target: str,
29+
db_session: Session,
30+
):
2731
"""Create external communication conversation."""
2832

2933
plugin = plugin_service.get_active_instance(
@@ -38,10 +42,24 @@ def create_case_conversation(case: Case, conversation_target: str, db_session: S
3842

3943
conversation = None
4044

41-
if conversation_target:
45+
# This case is a thread version, we send a new messaged (threaded) to the conversation target
46+
# for the configured case type
47+
if conversation_target and not case.dedicated_channel:
4248
try:
4349
conversation = plugin.instance.create_threaded(
44-
case=case, conversation_id=conversation_target, db_session=db_session
50+
case=case,
51+
conversation_id=conversation_target,
52+
db_session=db_session,
53+
)
54+
except Exception as e:
55+
# TODO: consistency across exceptions
56+
log.exception(e)
57+
58+
# otherwise, it must be a channel based case.
59+
if case.dedicated_channel:
60+
try:
61+
conversation = plugin.instance.create(
62+
name=f"case-{case.name}",
4563
)
4664
except Exception as e:
4765
# TODO: consistency across exceptions
@@ -53,11 +71,12 @@ def create_case_conversation(case: Case, conversation_target: str, db_session: S
5371

5472
conversation.update({"resource_type": plugin.plugin.slug, "resource_id": conversation["id"]})
5573

74+
print(f"got convo: {conversation}")
5675
conversation_in = ConversationCreate(
5776
resource_id=conversation["resource_id"],
5877
resource_type=conversation["resource_type"],
5978
weblink=conversation["weblink"],
60-
thread_id=conversation["timestamp"],
79+
thread_id=conversation.get("timestamp"),
6180
channel_id=conversation["id"],
6281
)
6382
case.conversation = create(db_session=db_session, conversation_in=conversation_in)
@@ -210,16 +229,23 @@ def set_conversation_topic(incident: Incident, db_session: SessionLocal):
210229
log.exception(e)
211230

212231

213-
def add_conversation_bookmark(incident: Incident, resource: Resource, db_session: SessionLocal):
232+
def add_conversation_bookmark(
233+
db_session: Session,
234+
subject: Subject,
235+
resource: Resource,
236+
title: str | None = None,
237+
):
214238
"""Adds a conversation bookmark."""
215-
if not incident.conversation:
239+
if not subject.conversation:
216240
log.warning(
217-
f"Conversation bookmark {resource.name.lower()} not added. No conversation available for this incident."
241+
f"Conversation bookmark {resource.name.lower()} not added. No conversation available."
218242
)
219243
return
220244

221245
plugin = plugin_service.get_active_instance(
222-
db_session=db_session, project_id=incident.project.id, plugin_type="conversation"
246+
db_session=db_session,
247+
project_id=subject.project.id,
248+
plugin_type="conversation",
223249
)
224250
if not plugin:
225251
log.warning(
@@ -228,109 +254,34 @@ def add_conversation_bookmark(incident: Incident, resource: Resource, db_session
228254
return
229255

230256
try:
231-
title = deslug_and_capitalize_resource_type(resource.resource_type)
257+
if not title:
258+
title = deslug_and_capitalize_resource_type(resource.resource_type)
232259
(
233260
plugin.instance.add_bookmark(
234-
incident.conversation.channel_id,
261+
subject.conversation.channel_id,
235262
resource.weblink,
236263
title=title,
237264
)
238265
if resource
239266
else log.warning(
240-
f"{resource.name} bookmark not added. No {resource.name.lower()} available for this incident."
267+
f"{resource.name} bookmark not added. No {resource.name.lower()} available for subject.."
241268
)
242269
)
243270
except Exception as e:
244271
event_service.log_incident_event(
245272
db_session=db_session,
246273
source="Dispatch Core App",
247274
description=f"Adding the {resource.name.lower()} bookmark failed. Reason: {e}",
248-
incident_id=incident.id,
275+
incident_id=subject.id,
249276
)
250277
log.exception(e)
251278

252279

253-
def add_conversation_bookmarks(incident: Incident, db_session: SessionLocal):
254-
"""Adds the conversation bookmarks."""
255-
if not incident.conversation:
256-
log.warning(
257-
"Conversation bookmarks not added. No conversation available for this incident."
258-
)
259-
return
260-
261-
plugin = plugin_service.get_active_instance(
262-
db_session=db_session, project_id=incident.project.id, plugin_type="conversation"
263-
)
264-
if not plugin:
265-
log.warning("Conversation bookmarks not added. No conversation plugin enabled.")
266-
return
267-
268-
try:
269-
(
270-
plugin.instance.add_bookmark(
271-
incident.conversation.channel_id,
272-
resolve_attr(incident, "incident_document.weblink"),
273-
title="Incident Document",
274-
)
275-
if incident.incident_document
276-
else log.warning(
277-
"Incident document bookmark not added. No document available for this incident."
278-
)
279-
)
280-
281-
(
282-
plugin.instance.add_bookmark(
283-
incident.conversation.channel_id,
284-
resolve_attr(incident, "conference.weblink"),
285-
title="Video Conference",
286-
)
287-
if incident.conference
288-
else log.warning(
289-
"Conference bookmark not added. No conference available for this incident."
290-
)
291-
)
292-
293-
(
294-
plugin.instance.add_bookmark(
295-
incident.conversation.channel_id,
296-
resolve_attr(incident, "storage.weblink"),
297-
title="Storage Folder",
298-
)
299-
if incident.storage
300-
else log.warning("Storage bookmark not added. No storage available for this incident.")
301-
)
302-
303-
ticket_weblink = resolve_attr(incident, "ticket.weblink")
304-
(
305-
plugin.instance.add_bookmark(
306-
incident.conversation.channel_id,
307-
ticket_weblink,
308-
title="Ticket",
309-
)
310-
if incident.ticket
311-
else log.warning("Ticket bookmark not added. No ticket available for this incident.")
312-
)
313-
314-
dispatch_weblink = f"{DISPATCH_UI_URL}/{incident.project.organization.name}/incidents/{incident.name}?project={incident.project.name}"
315-
316-
# only add Dispatch UI ticket if not using Dispatch ticket plugin
317-
if ticket_weblink != dispatch_weblink:
318-
plugin.instance.add_bookmark(
319-
incident.conversation.channel_id,
320-
dispatch_weblink,
321-
title="Dispatch UI",
322-
)
323-
except Exception as e:
324-
event_service.log_incident_event(
325-
db_session=db_session,
326-
source="Dispatch Core App",
327-
description=f"Adding the incident conversation bookmarks failed. Reason: {e}",
328-
incident_id=incident.id,
329-
)
330-
log.exception(e)
331-
332-
333-
def add_case_participants(case: Case, participant_emails: List[str], db_session: SessionLocal):
280+
def add_case_participants(
281+
case: Case,
282+
participant_emails: list[str],
283+
db_session: Session,
284+
):
334285
"""Adds one or more participants to the case conversation."""
335286
if not case.conversation:
336287
log.warning(
@@ -364,7 +315,9 @@ def add_case_participants(case: Case, participant_emails: List[str], db_session:
364315

365316

366317
def add_incident_participants(
367-
incident: Incident, participant_emails: List[str], db_session: SessionLocal
318+
incident: Incident,
319+
participant_emails: list[str],
320+
db_session: Session,
368321
):
369322
"""Adds one or more participants to the incident conversation."""
370323
if not incident.conversation:

src/dispatch/conversation/service.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,20 @@ def get_by_channel_id_ignoring_channel_type(
1515
Gets a conversation by its id ignoring the channel type, and updates the
1616
channel id in the database if the channel type has changed.
1717
"""
18-
channel_id_without_type = channel_id[1:]
19-
2018
conversation = None
2119

22-
query = db_session.query(Conversation).filter(
23-
Conversation.channel_id.contains(channel_id_without_type)
24-
)
20+
conversations = db_session.query(Conversation).filter(Conversation.channel_id == channel_id)
2521

2622
# The code below disambiguates between incident threads, case threads, and incident messages
2723
if not thread_id:
2824
# assume incident message
29-
conversation = query.first()
25+
conversation = conversations.first()
3026

3127
if not conversation:
32-
conversation = query.filter(Conversation.thread_id == thread_id).one_or_none()
28+
conversation = conversations.filter(Conversation.thread_id == thread_id).one_or_none()
3329

3430
if not conversation:
35-
conversation = query.one_or_none()
31+
conversation = conversations.one_or_none()
3632

3733
if conversation:
3834
if channel_id[0] != conversation.channel_id[0]:
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Creates a column for `dedicated_channel` on the Case model to support Cases
2+
in dedicated coversation channel (as opposed to only threads).
3+
4+
Revision ID: 3a33bc153e7e
5+
Revises: 71cb25c06fa0
6+
Create Date: 2024-04-09 16:28:06.148971
7+
8+
"""
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
13+
# revision identifiers, used by Alembic.
14+
revision = "3a33bc153e7e"
15+
down_revision = "71cb25c06fa0"
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
# ### commands auto generated by Alembic - please adjust! ###
22+
op.add_column("case", sa.Column("dedicated_channel", sa.Boolean(), nullable=True))
23+
# ### end Alembic commands ###
24+
25+
26+
def downgrade():
27+
# ### commands auto generated by Alembic - please adjust! ###
28+
op.drop_column("case", "dedicated_channel")
29+
# ### end Alembic commands ###

src/dispatch/enums.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,9 @@ class EventType(DispatchEnum):
6565
participant_updated = "Participant updated" # for added/removed users and role changes
6666
imported_message = "Imported message" # for stopwatch-reacted messages from Slack
6767
custom_event = "Custom event" # for user-added events (new feature)
68+
69+
70+
class SubjectNames(DispatchEnum):
71+
CASE = "Case"
72+
INCIDENT = "Incident"
73+
SIGNAL = "Signal"

src/dispatch/group/flows.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,9 @@ def update_group(
9797
):
9898
"""Updates an existing group."""
9999
if group is None:
100-
log.warning(f"Group not updated. No group provided. Cannot {group_action} for {group_member}.")
100+
log.warning(
101+
f"Group not updated. No group provided. Cannot {group_action} for {group_member}."
102+
)
101103
return
102104

103105
plugin = plugin_service.get_active_instance(

0 commit comments

Comments
 (0)