Skip to content

Commit 0a1c891

Browse files
authored
Adds ability to snooze Slack reminders for tactical and executive reports (Netflix#3553)
1 parent d558c30 commit 0a1c891

File tree

14 files changed

+244
-8
lines changed

14 files changed

+244
-8
lines changed

pyproject.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ line-length = 100
5151
# Allow unused variables when underscore-prefixed.
5252
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
5353

54-
# Assume Python 3.10.
55-
target-version = "py39"
54+
# Assume Python 3.11
55+
target-version = "py311"
5656

5757
[tool.ruff.mccabe]
5858
# Unlike Flake8, default to a complexity level of 10.

src/dispatch/conversation/enums.py

+1
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ class ConversationButtonActions(DispatchEnum):
1818
feedback_notification_provide = "feedback-notification-provide"
1919
update_task_status = "update-task-status"
2020
monitor_link = "monitor-link"
21+
remind_again = "remind-again"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Adds reminder delay time to Incident model
2+
3+
Revision ID: a3fb1380cf76
4+
Revises: fa23324d5679
5+
Create Date: 2023-07-05 14:27:32.239616
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
# revision identifiers, used by Alembic.
12+
revision = "a3fb1380cf76"
13+
down_revision = "fa23324d5679"
14+
branch_labels = None
15+
depends_on = None
16+
17+
18+
def upgrade():
19+
# ### commands auto generated by Alembic - please adjust! ###
20+
op.add_column(
21+
"incident", sa.Column("delay_executive_report_reminder", sa.DateTime(), nullable=True)
22+
)
23+
op.add_column(
24+
"incident", sa.Column("delay_tactical_report_reminder", sa.DateTime(), nullable=True)
25+
)
26+
# ### end Alembic commands ###
27+
28+
29+
def downgrade():
30+
# ### commands auto generated by Alembic - please adjust! ###
31+
op.drop_column("incident", "delay_tactical_report_reminder")
32+
op.drop_column("incident", "delay_executive_report_reminder")
33+
# ### end Alembic commands ###

src/dispatch/database/service.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ def create_sort_spec(model, sort_by, descending):
407407
"""Creates sort_spec."""
408408
sort_spec = []
409409
if sort_by and descending:
410-
for field, direction in zip(sort_by, descending):
410+
for field, direction in zip(sort_by, descending, strict=False):
411411
direction = "desc" if direction else "asc"
412412

413413
# we have a complex field, we may need to join

src/dispatch/decorators.py

+4
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ def _execute_task_in_project_context(
4242
kwargs["project"] = project
4343
func(*args, **kwargs)
4444
except Exception as e:
45+
log.error(
46+
f"Error trying to execute task: {fullname(func)} with parameters {args} and {kwargs}"
47+
)
4548
log.exception(e)
4649
finally:
4750
schema_session.close()
@@ -52,6 +55,7 @@ def _execute_task_in_project_context(
5255
)
5356
except Exception as e:
5457
# No rollback necessary as we only read from the database
58+
log.error(f"Error trying to execute task: {fullname(func)}")
5559
log.exception(e)
5660
finally:
5761
db_session.close()

src/dispatch/incident/models.py

+6
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ class Incident(Base, TimeStampMixin, ProjectMixin):
8383
participants_location = Column(String)
8484
commanders_location = Column(String)
8585
reporters_location = Column(String)
86+
delay_executive_report_reminder = Column(DateTime, nullable=True)
87+
delay_tactical_report_reminder = Column(DateTime, nullable=True)
8688

8789
# auto generated
8890
reported_at = Column(DateTime, default=datetime.utcnow)
@@ -309,6 +311,8 @@ class IncidentReadMinimal(IncidentBase):
309311
class IncidentUpdate(IncidentBase):
310312
cases: Optional[List[CaseRead]] = []
311313
commander: Optional[ParticipantUpdate]
314+
delay_executive_report_reminder: Optional[datetime] = None
315+
delay_tactical_report_reminder: Optional[datetime] = None
312316
duplicates: Optional[List[IncidentReadMinimal]] = []
313317
incident_costs: Optional[List[IncidentCostUpdate]] = []
314318
incident_priority: IncidentPriorityBase
@@ -345,6 +349,8 @@ class IncidentRead(IncidentBase):
345349
conference: Optional[ConferenceRead] = None
346350
conversation: Optional[ConversationRead] = None
347351
created_at: Optional[datetime] = None
352+
delay_executive_report_reminder: Optional[datetime] = None
353+
delay_tactical_report_reminder: Optional[datetime] = None
348354
documents: Optional[List[DocumentRead]] = []
349355
duplicates: Optional[List[IncidentReadMinimal]] = []
350356
events: Optional[List[EventRead]] = []

src/dispatch/messaging/strings.py

+49
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@
1010
from dispatch import config
1111
from dispatch.enums import DispatchEnum, DocumentResourceTypes, DocumentResourceReferenceTypes
1212

13+
"""Dict for reminder strings and values. Note values are in hours"""
14+
reminder_select_values = {
15+
"thirty": {"message": "30 minutes", "value": 0.5},
16+
"one_hour": {"message": "1 hour", "value": 1},
17+
"two_hours": {"message": "2 hours", "value": 2},
18+
}
19+
1320

1421
class MessageType(DispatchEnum):
1522
evergreen_reminder = "evergreen-reminder"
@@ -218,6 +225,11 @@ class MessageType(DispatchEnum):
218225
"\n", " "
219226
).strip()
220227

228+
INCIDENT_REPORT_REMINDER_DELAYED_DESCRIPTION = """You asked me to send you this reminder to write a {{report_type}} for this incident.
229+
You can use `{{command}}` in the conversation to assist you in writing one.""".replace(
230+
"\n", " "
231+
).strip()
232+
221233
INCIDENT_CLOSE_REMINDER_DESCRIPTION = """The status of this incident hasn't been updated recently.
222234
You can use `{{command}}` in the conversation to close the incident if it has been resolved and can be closed.""".replace(
223235
"\n", " "
@@ -531,13 +543,39 @@ class MessageType(DispatchEnum):
531543
{"title": "Next Steps", "text": "{{next_steps}}"},
532544
]
533545

546+
REMIND_AGAIN_OPTIONS = {
547+
"text": "[Optional] Remind me again in:",
548+
"select": {
549+
"placeholder": "Choose a time value",
550+
"select_action": ConversationButtonActions.remind_again,
551+
"options": [
552+
{
553+
"option_text": value["message"],
554+
"option_value": "{{organization_slug}}-{{incident_id}}-{{report_type}}-" + key,
555+
}
556+
for key, value in reminder_select_values.items()
557+
],
558+
},
559+
}
560+
534561
INCIDENT_REPORT_REMINDER = [
535562
{
536563
"title": "{{name}} Incident - {{report_type}} Reminder",
537564
"title_link": "{{ticket_weblink}}",
538565
"text": INCIDENT_REPORT_REMINDER_DESCRIPTION,
539566
},
540567
INCIDENT_TITLE,
568+
REMIND_AGAIN_OPTIONS,
569+
]
570+
571+
INCIDENT_REPORT_REMINDER_DELAYED = [
572+
{
573+
"title": "{{name}} Incident - {{report_type}} Reminder",
574+
"title_link": "{{ticket_weblink}}",
575+
"text": INCIDENT_REPORT_REMINDER_DELAYED_DESCRIPTION,
576+
},
577+
INCIDENT_TITLE,
578+
REMIND_AGAIN_OPTIONS,
541579
]
542580

543581

@@ -764,6 +802,17 @@ def render_message_template(message_template: List[dict], **kwargs):
764802
if button.get("button_url"):
765803
button["button_url"] = env.from_string(button["button_url"]).render(**kwargs)
766804

805+
# render drop-down list
806+
if select := d.get("select"):
807+
if placeholder := select.get("placeholder"):
808+
select["placeholder"] = env.from_string(placeholder).render(**kwargs)
809+
810+
select["select_action"] = env.from_string(select["select_action"]).render(**kwargs)
811+
812+
for option in select["options"]:
813+
option["option_text"] = env.from_string(option["option_text"]).render(**kwargs)
814+
option["option_value"] = env.from_string(option["option_value"]).render(**kwargs)
815+
767816
if d.get("visibility_mapping"):
768817
d["text"] = d["visibility_mapping"][kwargs["visibility"]]
769818

src/dispatch/plugins/dispatch_slack/bolt.py

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
configuration_middleware,
2828
message_context_middleware,
2929
user_middleware,
30+
select_context_middleware,
3031
)
3132

3233
app = App(token="xoxb-valid", request_verification_enabled=False, token_verification_enabled=False)
@@ -139,6 +140,7 @@ def build_and_log_error(
139140
message_context_middleware,
140141
user_middleware,
141142
configuration_middleware,
143+
select_context_middleware,
142144
],
143145
)
144146
def handle_message_events(

src/dispatch/plugins/dispatch_slack/incident/enums.py

+4
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,7 @@ class UpdateNotificationGroupActionIds(DispatchEnum):
9696

9797
class UpdateNotificationGroupBlockIds(DispatchEnum):
9898
members = "update-notification-group-members"
99+
100+
101+
class RemindAgainActions(DispatchEnum):
102+
submit = ConversationButtonActions.remind_again

src/dispatch/plugins/dispatch_slack/incident/interactive.py

+54-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
2-
from datetime import datetime
2+
import uuid
3+
from datetime import datetime, timedelta
34
from typing import Any
45

56
import pytz
@@ -79,6 +80,7 @@
7980
IncidentUpdateActions,
8081
LinkMonitorActionIds,
8182
LinkMonitorBlockIds,
83+
RemindAgainActions,
8284
ReportExecutiveActions,
8385
ReportExecutiveBlockIds,
8486
ReportTacticalActions,
@@ -103,6 +105,7 @@
103105
restricted_command_middleware,
104106
subject_middleware,
105107
user_middleware,
108+
select_context_middleware,
106109
)
107110
from dispatch.plugins.dispatch_slack.modals.common import send_success_modal
108111
from dispatch.plugins.dispatch_slack.models import MonitorMetadata, TaskMetadata
@@ -121,6 +124,8 @@
121124
from dispatch.task.enums import TaskStatus
122125
from dispatch.task.models import Task
123126
from dispatch.ticket import flows as ticket_flows
127+
from dispatch.messaging.strings import reminder_select_values
128+
from dispatch.plugins.dispatch_slack.messaging import build_unexpected_error_message
124129

125130
log = logging.getLogger(__file__)
126131

@@ -2278,3 +2283,51 @@ def handle_update_task_status_button_click(
22782283
view_id=body["view"]["id"],
22792284
tasks=tasks,
22802285
)
2286+
2287+
2288+
@app.action(RemindAgainActions.submit, middleware=[select_context_middleware, db_middleware])
2289+
def handle_remind_again_select_action(
2290+
ack: Ack,
2291+
body: dict,
2292+
context: BoltContext,
2293+
db_session: Session,
2294+
respond: Respond,
2295+
user: DispatchUser,
2296+
) -> None:
2297+
"""Handles remind again select event."""
2298+
ack()
2299+
try:
2300+
incident = incident_service.get(db_session=db_session, incident_id=context["subject"].id)
2301+
2302+
# User-selected option as org-id-report_type-delay
2303+
value = body["actions"][0]["selected_option"]["value"]
2304+
2305+
# Parse out report type and selected delay
2306+
*_, report_type, selection = value.split("-")
2307+
selection_as_message = reminder_select_values[selection]["message"]
2308+
hours = reminder_select_values[selection]["value"]
2309+
2310+
# Get new remind time
2311+
delay_to_time = datetime.utcnow() + timedelta(hours=hours)
2312+
2313+
# Store in incident
2314+
if report_type == ReportTypes.tactical_report:
2315+
incident.delay_tactical_report_reminder = delay_to_time
2316+
elif report_type == ReportTypes.executive_report:
2317+
incident.delay_executive_report_reminder = delay_to_time
2318+
2319+
db_session.add(incident)
2320+
db_session.commit()
2321+
2322+
message = f"Success! We'll remind you again in {selection_as_message}."
2323+
respond(
2324+
text=message, response_type="ephemeral", replace_original=False, delete_original=False
2325+
)
2326+
except Exception as e:
2327+
guid = str(uuid.uuid4())
2328+
log.error(f"ERROR trying to save reminder delay with guid {guid}.")
2329+
log.exception(e)
2330+
message = build_unexpected_error_message(guid)
2331+
respond(
2332+
text=message, response_type="ephemeral", replace_original=False, delete_original=False
2333+
)

src/dispatch/plugins/dispatch_slack/messaging.py

+32-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,16 @@
77
import logging
88
from typing import Any, List, Optional
99

10-
from blockkit import Actions, Button, Context, Divider, MarkdownText, Section
10+
from blockkit import (
11+
Actions,
12+
Button,
13+
Context,
14+
Divider,
15+
MarkdownText,
16+
Section,
17+
StaticSelect,
18+
PlainOption,
19+
)
1120
from slack_sdk.web.client import WebClient
1221
from slack_sdk.errors import SlackApiError
1322

@@ -219,6 +228,28 @@ def default_notification(items: list):
219228

220229
elements.append(element)
221230
blocks.append(Actions(elements=elements))
231+
232+
if select := item.get("select"):
233+
options = []
234+
for option in select["options"]:
235+
element = PlainOption(text=option["option_text"], value=option["option_value"])
236+
options.append(element)
237+
238+
static_select = []
239+
if select.get("placeholder"):
240+
static_select.append(
241+
StaticSelect(
242+
placeholder=select["placeholder"],
243+
options=options,
244+
action_id=select["select_action"],
245+
)
246+
)
247+
else:
248+
static_select.append(
249+
StaticSelect(options=options, action_id=select["select_action"])
250+
)
251+
blocks.append(Actions(elements=static_select))
252+
222253
return blocks
223254

224255

src/dispatch/plugins/dispatch_slack/middleware.py

+11
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,17 @@ def resolve_context_from_conversation(channel_id: str) -> Optional[Subject]:
6060
scoped_db_session.close()
6161

6262

63+
def select_context_middleware(payload: dict, context: BoltContext, next: Callable) -> None:
64+
"""Attempt to determine the current context of the selection."""
65+
organization_slug, incident_id, *_ = payload["selected_option"]["value"].split("-")
66+
subject_data = SubjectMetadata(
67+
organization_slug=organization_slug, id=incident_id, type="Incident"
68+
)
69+
70+
context.update({"subject": subject_data})
71+
next()
72+
73+
6374
def shortcut_context_middleware(context: BoltContext, next: Callable) -> None:
6475
"""Attempts to determine the current context of the event."""
6576
context.update({"subject": SubjectMetadata(channel_id=context.channel_id)})

0 commit comments

Comments
 (0)