Skip to content

Commit

Permalink
Adds ability to snooze Slack reminders for tactical and executive rep…
Browse files Browse the repository at this point in the history
…orts (#3553)
  • Loading branch information
whitdog47 authored Jul 11, 2023
1 parent d558c30 commit 0a1c891
Show file tree
Hide file tree
Showing 14 changed files with 244 additions and 8 deletions.
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ line-length = 100
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"

# Assume Python 3.10.
target-version = "py39"
# Assume Python 3.11
target-version = "py311"

[tool.ruff.mccabe]
# Unlike Flake8, default to a complexity level of 10.
Expand Down
1 change: 1 addition & 0 deletions src/dispatch/conversation/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ class ConversationButtonActions(DispatchEnum):
feedback_notification_provide = "feedback-notification-provide"
update_task_status = "update-task-status"
monitor_link = "monitor-link"
remind_again = "remind-again"
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Adds reminder delay time to Incident model
Revision ID: a3fb1380cf76
Revises: fa23324d5679
Create Date: 2023-07-05 14:27:32.239616
"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = "a3fb1380cf76"
down_revision = "fa23324d5679"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"incident", sa.Column("delay_executive_report_reminder", sa.DateTime(), nullable=True)
)
op.add_column(
"incident", sa.Column("delay_tactical_report_reminder", sa.DateTime(), nullable=True)
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("incident", "delay_tactical_report_reminder")
op.drop_column("incident", "delay_executive_report_reminder")
# ### end Alembic commands ###
2 changes: 1 addition & 1 deletion src/dispatch/database/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ def create_sort_spec(model, sort_by, descending):
"""Creates sort_spec."""
sort_spec = []
if sort_by and descending:
for field, direction in zip(sort_by, descending):
for field, direction in zip(sort_by, descending, strict=False):
direction = "desc" if direction else "asc"

# we have a complex field, we may need to join
Expand Down
4 changes: 4 additions & 0 deletions src/dispatch/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ def _execute_task_in_project_context(
kwargs["project"] = project
func(*args, **kwargs)
except Exception as e:
log.error(
f"Error trying to execute task: {fullname(func)} with parameters {args} and {kwargs}"
)
log.exception(e)
finally:
schema_session.close()
Expand All @@ -52,6 +55,7 @@ def _execute_task_in_project_context(
)
except Exception as e:
# No rollback necessary as we only read from the database
log.error(f"Error trying to execute task: {fullname(func)}")
log.exception(e)
finally:
db_session.close()
Expand Down
6 changes: 6 additions & 0 deletions src/dispatch/incident/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ class Incident(Base, TimeStampMixin, ProjectMixin):
participants_location = Column(String)
commanders_location = Column(String)
reporters_location = Column(String)
delay_executive_report_reminder = Column(DateTime, nullable=True)
delay_tactical_report_reminder = Column(DateTime, nullable=True)

# auto generated
reported_at = Column(DateTime, default=datetime.utcnow)
Expand Down Expand Up @@ -309,6 +311,8 @@ class IncidentReadMinimal(IncidentBase):
class IncidentUpdate(IncidentBase):
cases: Optional[List[CaseRead]] = []
commander: Optional[ParticipantUpdate]
delay_executive_report_reminder: Optional[datetime] = None
delay_tactical_report_reminder: Optional[datetime] = None
duplicates: Optional[List[IncidentReadMinimal]] = []
incident_costs: Optional[List[IncidentCostUpdate]] = []
incident_priority: IncidentPriorityBase
Expand Down Expand Up @@ -345,6 +349,8 @@ class IncidentRead(IncidentBase):
conference: Optional[ConferenceRead] = None
conversation: Optional[ConversationRead] = None
created_at: Optional[datetime] = None
delay_executive_report_reminder: Optional[datetime] = None
delay_tactical_report_reminder: Optional[datetime] = None
documents: Optional[List[DocumentRead]] = []
duplicates: Optional[List[IncidentReadMinimal]] = []
events: Optional[List[EventRead]] = []
Expand Down
49 changes: 49 additions & 0 deletions src/dispatch/messaging/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@
from dispatch import config
from dispatch.enums import DispatchEnum, DocumentResourceTypes, DocumentResourceReferenceTypes

"""Dict for reminder strings and values. Note values are in hours"""
reminder_select_values = {
"thirty": {"message": "30 minutes", "value": 0.5},
"one_hour": {"message": "1 hour", "value": 1},
"two_hours": {"message": "2 hours", "value": 2},
}


class MessageType(DispatchEnum):
evergreen_reminder = "evergreen-reminder"
Expand Down Expand Up @@ -218,6 +225,11 @@ class MessageType(DispatchEnum):
"\n", " "
).strip()

INCIDENT_REPORT_REMINDER_DELAYED_DESCRIPTION = """You asked me to send you this reminder to write a {{report_type}} for this incident.
You can use `{{command}}` in the conversation to assist you in writing one.""".replace(
"\n", " "
).strip()

INCIDENT_CLOSE_REMINDER_DESCRIPTION = """The status of this incident hasn't been updated recently.
You can use `{{command}}` in the conversation to close the incident if it has been resolved and can be closed.""".replace(
"\n", " "
Expand Down Expand Up @@ -531,13 +543,39 @@ class MessageType(DispatchEnum):
{"title": "Next Steps", "text": "{{next_steps}}"},
]

REMIND_AGAIN_OPTIONS = {
"text": "[Optional] Remind me again in:",
"select": {
"placeholder": "Choose a time value",
"select_action": ConversationButtonActions.remind_again,
"options": [
{
"option_text": value["message"],
"option_value": "{{organization_slug}}-{{incident_id}}-{{report_type}}-" + key,
}
for key, value in reminder_select_values.items()
],
},
}

INCIDENT_REPORT_REMINDER = [
{
"title": "{{name}} Incident - {{report_type}} Reminder",
"title_link": "{{ticket_weblink}}",
"text": INCIDENT_REPORT_REMINDER_DESCRIPTION,
},
INCIDENT_TITLE,
REMIND_AGAIN_OPTIONS,
]

INCIDENT_REPORT_REMINDER_DELAYED = [
{
"title": "{{name}} Incident - {{report_type}} Reminder",
"title_link": "{{ticket_weblink}}",
"text": INCIDENT_REPORT_REMINDER_DELAYED_DESCRIPTION,
},
INCIDENT_TITLE,
REMIND_AGAIN_OPTIONS,
]


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

# render drop-down list
if select := d.get("select"):
if placeholder := select.get("placeholder"):
select["placeholder"] = env.from_string(placeholder).render(**kwargs)

select["select_action"] = env.from_string(select["select_action"]).render(**kwargs)

for option in select["options"]:
option["option_text"] = env.from_string(option["option_text"]).render(**kwargs)
option["option_value"] = env.from_string(option["option_value"]).render(**kwargs)

if d.get("visibility_mapping"):
d["text"] = d["visibility_mapping"][kwargs["visibility"]]

Expand Down
2 changes: 2 additions & 0 deletions src/dispatch/plugins/dispatch_slack/bolt.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
configuration_middleware,
message_context_middleware,
user_middleware,
select_context_middleware,
)

app = App(token="xoxb-valid", request_verification_enabled=False, token_verification_enabled=False)
Expand Down Expand Up @@ -139,6 +140,7 @@ def build_and_log_error(
message_context_middleware,
user_middleware,
configuration_middleware,
select_context_middleware,
],
)
def handle_message_events(
Expand Down
4 changes: 4 additions & 0 deletions src/dispatch/plugins/dispatch_slack/incident/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,7 @@ class UpdateNotificationGroupActionIds(DispatchEnum):

class UpdateNotificationGroupBlockIds(DispatchEnum):
members = "update-notification-group-members"


class RemindAgainActions(DispatchEnum):
submit = ConversationButtonActions.remind_again
55 changes: 54 additions & 1 deletion src/dispatch/plugins/dispatch_slack/incident/interactive.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
from datetime import datetime
import uuid
from datetime import datetime, timedelta
from typing import Any

import pytz
Expand Down Expand Up @@ -79,6 +80,7 @@
IncidentUpdateActions,
LinkMonitorActionIds,
LinkMonitorBlockIds,
RemindAgainActions,
ReportExecutiveActions,
ReportExecutiveBlockIds,
ReportTacticalActions,
Expand All @@ -103,6 +105,7 @@
restricted_command_middleware,
subject_middleware,
user_middleware,
select_context_middleware,
)
from dispatch.plugins.dispatch_slack.modals.common import send_success_modal
from dispatch.plugins.dispatch_slack.models import MonitorMetadata, TaskMetadata
Expand All @@ -121,6 +124,8 @@
from dispatch.task.enums import TaskStatus
from dispatch.task.models import Task
from dispatch.ticket import flows as ticket_flows
from dispatch.messaging.strings import reminder_select_values
from dispatch.plugins.dispatch_slack.messaging import build_unexpected_error_message

log = logging.getLogger(__file__)

Expand Down Expand Up @@ -2278,3 +2283,51 @@ def handle_update_task_status_button_click(
view_id=body["view"]["id"],
tasks=tasks,
)


@app.action(RemindAgainActions.submit, middleware=[select_context_middleware, db_middleware])
def handle_remind_again_select_action(
ack: Ack,
body: dict,
context: BoltContext,
db_session: Session,
respond: Respond,
user: DispatchUser,
) -> None:
"""Handles remind again select event."""
ack()
try:
incident = incident_service.get(db_session=db_session, incident_id=context["subject"].id)

# User-selected option as org-id-report_type-delay
value = body["actions"][0]["selected_option"]["value"]

# Parse out report type and selected delay
*_, report_type, selection = value.split("-")
selection_as_message = reminder_select_values[selection]["message"]
hours = reminder_select_values[selection]["value"]

# Get new remind time
delay_to_time = datetime.utcnow() + timedelta(hours=hours)

# Store in incident
if report_type == ReportTypes.tactical_report:
incident.delay_tactical_report_reminder = delay_to_time
elif report_type == ReportTypes.executive_report:
incident.delay_executive_report_reminder = delay_to_time

db_session.add(incident)
db_session.commit()

message = f"Success! We'll remind you again in {selection_as_message}."
respond(
text=message, response_type="ephemeral", replace_original=False, delete_original=False
)
except Exception as e:
guid = str(uuid.uuid4())
log.error(f"ERROR trying to save reminder delay with guid {guid}.")
log.exception(e)
message = build_unexpected_error_message(guid)
respond(
text=message, response_type="ephemeral", replace_original=False, delete_original=False
)
33 changes: 32 additions & 1 deletion src/dispatch/plugins/dispatch_slack/messaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,16 @@
import logging
from typing import Any, List, Optional

from blockkit import Actions, Button, Context, Divider, MarkdownText, Section
from blockkit import (
Actions,
Button,
Context,
Divider,
MarkdownText,
Section,
StaticSelect,
PlainOption,
)
from slack_sdk.web.client import WebClient
from slack_sdk.errors import SlackApiError

Expand Down Expand Up @@ -219,6 +228,28 @@ def default_notification(items: list):

elements.append(element)
blocks.append(Actions(elements=elements))

if select := item.get("select"):
options = []
for option in select["options"]:
element = PlainOption(text=option["option_text"], value=option["option_value"])
options.append(element)

static_select = []
if select.get("placeholder"):
static_select.append(
StaticSelect(
placeholder=select["placeholder"],
options=options,
action_id=select["select_action"],
)
)
else:
static_select.append(
StaticSelect(options=options, action_id=select["select_action"])
)
blocks.append(Actions(elements=static_select))

return blocks


Expand Down
11 changes: 11 additions & 0 deletions src/dispatch/plugins/dispatch_slack/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ def resolve_context_from_conversation(channel_id: str) -> Optional[Subject]:
scoped_db_session.close()


def select_context_middleware(payload: dict, context: BoltContext, next: Callable) -> None:
"""Attempt to determine the current context of the selection."""
organization_slug, incident_id, *_ = payload["selected_option"]["value"].split("-")
subject_data = SubjectMetadata(
organization_slug=organization_slug, id=incident_id, type="Incident"
)

context.update({"subject": subject_data})
next()


def shortcut_context_middleware(context: BoltContext, next: Callable) -> None:
"""Attempts to determine the current context of the event."""
context.update({"subject": SubjectMetadata(channel_id=context.channel_id)})
Expand Down
Loading

0 comments on commit 0a1c891

Please sign in to comment.