Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds reminder to oncall end-of-shift feedback request #3773

Merged
merged 8 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Adds reminders to oncall shift feedback

Revision ID: 0560fab4537f
Revises: 0356472ea980
Create Date: 2023-09-08 17:13:57.903367

"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = "0560fab4537f"
down_revision = "0356472ea980"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"service_feedback_reminder",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("reminder_at", sa.DateTime(), nullable=True),
sa.Column("schedule_id", sa.String(), nullable=True),
sa.Column("schedule_name", sa.String(), nullable=True),
sa.Column("shift_end_at", sa.DateTime(), nullable=True),
sa.Column("individual_contact_id", sa.Integer(), nullable=True),
sa.Column("project_id", sa.Integer(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("updated_at", sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(
["individual_contact_id"],
["individual_contact.id"],
),
sa.ForeignKeyConstraint(
["project_id"],
["project.id"],
),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("service_feedback_reminder")
# ### end Alembic commands ###
36 changes: 34 additions & 2 deletions src/dispatch/feedback/service/messaging.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import logging
from datetime import datetime, timedelta
from typing import Optional

from sqlalchemy.orm import Session

from dispatch.individual.models import IndividualContact
from dispatch.messaging.strings import (
ONCALL_SHIFT_FEEDBACK_NOTIFICATION,
ONCALL_SHIFT_FEEDBACK_NOTIFICATION_REMINDER,
MessageType,
)
from dispatch.plugin import service as plugin_service
from dispatch.project.models import Project

from .reminder.models import ServiceFeedbackReminder, ServiceFeedbackReminderUpdate
from .reminder import service as reminder_service

log = logging.getLogger(__name__)

Expand All @@ -21,6 +25,7 @@ def send_oncall_shift_feedback_message(
schedule_id: str,
shift_end_at: str,
schedule_name: str,
reminder: Optional[ServiceFeedbackReminder] = None,
db_session: Session,
):
"""
Expand All @@ -39,14 +44,41 @@ def send_oncall_shift_feedback_message(
)
return

if reminder:
# update reminder with 23 hours from now
reminder = reminder_service.update(
db_session=db_session,
reminder=reminder,
reminder_in=ServiceFeedbackReminderUpdate(
id=reminder.id,
reminder_at=datetime.utcnow() + timedelta(hours=23),
),
)
notification_template = ONCALL_SHIFT_FEEDBACK_NOTIFICATION_REMINDER
else:
# create reminder and pass to plugin
reminder = reminder_service.create(
db_session=db_session,
reminder_in=ServiceFeedbackReminder(
reminder_at=datetime.utcnow() + timedelta(hours=23),
individual_contact_id=individual.id,
project_id=project.id,
schedule_id=schedule_id,
schedule_name=schedule_name,
shift_end_at=shift_end_at,
),
)

shift_end_clean = shift_end_at.replace("T", " ").replace("Z", "")
items = [
{
"individual_name": individual.name,
"oncall_schedule_id": schedule_id,
"oncall_service_name": schedule_name,
"organization_slug": project.organization.slug,
"project_id": project.id,
"shift_end_at": shift_end_at,
"shift_end_at": shift_end_clean,
"reminder_id": reminder.id,
}
]

Expand Down
Empty file.
45 changes: 45 additions & 0 deletions src/dispatch/feedback/service/reminder/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from datetime import datetime
from typing import Optional

from sqlalchemy import Column, Integer, ForeignKey, DateTime, String

from dispatch.database.core import Base
from dispatch.individual.models import IndividualContactRead
from dispatch.models import DispatchBase, TimeStampMixin, PrimaryKey
from dispatch.project.models import ProjectRead


class ServiceFeedbackReminder(TimeStampMixin, Base):
# Columns
id = Column(Integer, primary_key=True)
reminder_at = Column(DateTime)
schedule_id = Column(String)
schedule_name = Column(String)
shift_end_at = Column(DateTime)

# Relationships
individual_contact_id = Column(Integer, ForeignKey("individual_contact.id"))
project_id = Column(Integer, ForeignKey("project.id"))


# Pydantic models
class ServiceFeedbackReminderBase(DispatchBase):
reminder_at: Optional[datetime]
individual: Optional[IndividualContactRead]
project: Optional[ProjectRead]
schedule_id: Optional[str]
schedule_name: Optional[str]
shift_end_at: Optional[datetime]


class ServiceFeedbackReminderCreate(ServiceFeedbackReminderBase):
pass


class ServiceFeedbackReminderUpdate(ServiceFeedbackReminderBase):
id: PrimaryKey = None
reminder_at: Optional[datetime]


class ServiceFeedbackReminderRead(ServiceFeedbackReminderBase):
id: PrimaryKey
59 changes: 59 additions & 0 deletions src/dispatch/feedback/service/reminder/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from typing import List, Optional
from datetime import datetime, timedelta

from .models import (
ServiceFeedbackReminder,
ServiceFeedbackReminderCreate,
ServiceFeedbackReminderUpdate,
)
from dispatch.individual.models import IndividualContact
from dispatch.project.models import Project


def get_all_expired_reminders_by_project_id(
*, db_session, project_id: int
) -> List[Optional[ServiceFeedbackReminder]]:
"""Returns all expired reminders by project id."""
return (
db_session.query(ServiceFeedbackReminder)
.join(IndividualContact)
.join(Project)
.filter(Project.id == project_id)
.filter(datetime.utcnow() >= ServiceFeedbackReminder.reminder_at - timedelta(minutes=1))
.all()
)


def create(*, db_session, reminder_in: ServiceFeedbackReminderCreate) -> ServiceFeedbackReminder:
"""Creates a new service feedback reminder."""
reminder = ServiceFeedbackReminder(**reminder_in.dict())

db_session.add(reminder_in)
db_session.commit()
return reminder


def update(
*, db_session, reminder: ServiceFeedbackReminder, reminder_in: ServiceFeedbackReminderUpdate
) -> ServiceFeedbackReminder:
"""Updates a service feedback reminder."""
reminder_data = reminder.dict()
update_data = reminder_in.dict(skip_defaults=True)

for field in reminder_data:
if field in update_data:
setattr(reminder, field, update_data[field])

db_session.commit()
return reminder


def delete(*, db_session, reminder_id: int):
"""Deletes a service feedback reminder."""
reminder = (
db_session.query(ServiceFeedbackReminder)
.filter(ServiceFeedbackReminder.id == reminder_id)
.one_or_none()
)
db_session.delete(reminder)
db_session.commit()
22 changes: 22 additions & 0 deletions src/dispatch/feedback/service/scheduled.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from dispatch.project.models import Project
from dispatch.scheduler import scheduler
from dispatch.service import service as service_service
from .reminder import service as reminder_service

from .messaging import send_oncall_shift_feedback_message

Expand All @@ -28,13 +29,34 @@
@scheduled_project_task
def oncall_shift_feedback_ucan(db_session: SessionLocal, project: Project):
oncall_shift_feedback(db_session=db_session, project=project)
find_expired_reminders_and_send(db_session=db_session, project=project)


@scheduler.add(every(1).day.at("06:00"), name="oncall-shift-feedback-emea")
@timer
@scheduled_project_task
def oncall_shift_feedback_emea(db_session: SessionLocal, project: Project):
oncall_shift_feedback(db_session=db_session, project=project)
find_expired_reminders_and_send(db_session=db_session, project=project)


def find_expired_reminders_and_send(*, db_session: SessionLocal, project: Project):
reminders = reminder_service.get_all_expired_reminders_by_project_id(
db_session=db_session, project_id=project.id
)
for reminder in reminders:
individual = individual_service.get(
db_session=db_session, individual_contact_id=reminder.individual_contact_id
)
send_oncall_shift_feedback_message(
project=project,
individual=individual,
schedule_id=reminder.schedule_id,
shift_end_at=str(reminder.shift_end_at),
schedule_name=reminder.schedule_name,
reminder=reminder,
db_session=db_session,
)


def find_schedule_and_send(
Expand Down
36 changes: 28 additions & 8 deletions src/dispatch/messaging/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,10 @@ class MessageType(DispatchEnum):
"""

ONCALL_SHIFT_FEEDBACK_DESCRIPTION = """
Hi {{ individual_name }}, it appears that your {{ oncall_service_name }} shift has completed. To help us understand the impact on our responders, we would appreciate your feedback."""
Hi {{ individual_name }}, it appears that your {{ oncall_service_name }} shift recently completed on {{ shift_end_at }} UTC. To help us understand the impact on our responders, we would appreciate your feedback."""

ONCALL_SHIFT_FEEDBACK_RECEIVED_DESCRIPTION = """
We received your feedback for your shift that ended {{ shift_end_at }} UTC. Thank you!"""

INCIDENT_STATUS_CHANGE_DESCRIPTION = """
The incident status has been changed from {{ incident_status_old }} to {{ incident_status_new }}.""".replace(
Expand Down Expand Up @@ -763,17 +766,34 @@ class MessageType(DispatchEnum):
}
]

ONCALL_SHIFT_FEEDBACK_BUTTONS = [
{
"button_text": "Provide Feedback",
"button_value": "{{organization_slug}}|{{project_id}}|{{oncall_schedule_id}}|{{shift_end_at}}|{{reminder_id}}",
"button_action": ConversationButtonActions.service_feedback,
}
]

ONCALL_SHIFT_FEEDBACK_NOTIFICATION = [
{
"title": "Oncall Shift Feedback",
"text": ONCALL_SHIFT_FEEDBACK_DESCRIPTION,
"buttons": [
{
"button_text": "Provide Feedback",
"button_value": "{{organization_slug}}|{{project_id}}|{{oncall_schedule_id}}|{{shift_end_at}}",
"button_action": ConversationButtonActions.service_feedback,
}
],
"buttons": ONCALL_SHIFT_FEEDBACK_BUTTONS,
}
]

ONCALL_SHIFT_FEEDBACK_NOTIFICATION_REMINDER = [
{
"title": "Oncall Shift Feedback - REMINDER",
"text": ONCALL_SHIFT_FEEDBACK_DESCRIPTION,
"buttons": ONCALL_SHIFT_FEEDBACK_BUTTONS,
}
]

ONCALL_SHIFT_FEEDBACK_RECEIVED = [
{
"title": "Oncall Shift Feedback - RECEIVED",
"text": ONCALL_SHIFT_FEEDBACK_RECEIVED_DESCRIPTION,
}
]

Expand Down
42 changes: 40 additions & 2 deletions src/dispatch/plugins/dispatch_slack/feedback/interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
from dispatch.feedback.service.models import ServiceFeedbackRating
from dispatch.incident import service as incident_service
from dispatch.participant import service as participant_service
from dispatch.feedback.service.reminder import service as reminder_service
from dispatch.plugin import service as plugin_service
from dispatch.plugins.dispatch_slack.bolt import app
from dispatch.plugins.dispatch_slack.fields import static_select_block
from dispatch.plugins.dispatch_slack.middleware import (
Expand All @@ -41,6 +43,10 @@
ServiceFeedbackNotificationActions,
ServiceFeedbackNotificationBlockIds,
)
from dispatch.messaging.strings import (
ONCALL_SHIFT_FEEDBACK_RECEIVED,
MessageType,
)

log = logging.getLogger(__file__)

Expand Down Expand Up @@ -378,11 +384,20 @@ def handle_oncall_shift_feedback_submission_event(
feedback = form_data.get(ServiceFeedbackNotificationBlockIds.feedback_input)
rating = form_data.get(ServiceFeedbackNotificationBlockIds.rating_select, {}).get("value")

# metadata is organization_slug|project_id|schedule_id|shift_end_at
# metadata is organization_slug|project_id|schedule_id|shift_end_at|reminder_id
metadata = body["view"]["private_metadata"].split("|")
project_id = metadata[1]
schedule_id = metadata[2]
shift_end_at = datetime.strptime(metadata[3], "%Y-%m-%dT%H:%M:%SZ")
shift_end_raw = metadata[3]
shift_end_at = (
datetime.strptime(shift_end_raw, "%Y-%m-%dT%H:%M:%SZ")
if "T" in shift_end_raw
else datetime.strptime(shift_end_raw, "%Y-%m-%d %H:%M:%S")
)
# if there's a reminder id, delete the reminder
if len(metadata) > 4:
reminder_id = metadata[4]
reminder_service.delete(db_session=db_session, reminder_id=reminder_id)

individual = (
None
Expand Down Expand Up @@ -416,3 +431,26 @@ def handle_oncall_shift_feedback_submission_event(
view_id=body["view"]["id"],
view=modal,
)

plugin = plugin_service.get_active_instance(
db_session=db_session, project_id=project_id, plugin_type="conversation"
)

if plugin:
notification_text = "Oncall Shift Feedback Received"
notification_template = ONCALL_SHIFT_FEEDBACK_RECEIVED
items = [
{
"shift_end_at": shift_end_at,
}
]
try:
plugin.instance.send_direct(
individual.email,
notification_text,
notification_template,
MessageType.service_feedback,
items=items,
)
except Exception as e:
log.exception(e)