Skip to content

Commit

Permalink
feat(case): adding case feedback (#5356)
Browse files Browse the repository at this point in the history
  • Loading branch information
whitdog47 authored Oct 18, 2024
1 parent d648a96 commit 1f34f83
Show file tree
Hide file tree
Showing 15 changed files with 325 additions and 23 deletions.
6 changes: 6 additions & 0 deletions src/dispatch/case/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from .messaging import (
send_case_created_notifications,
send_case_update_notifications,
send_case_rating_feedback_message,
)

from .models import Case, CaseStatus
Expand Down Expand Up @@ -498,6 +499,11 @@ def case_closed_status_flow(case: Case, db_session=None):
for document in case.documents:
document_flows.mark_document_as_readonly(document=document, db_session=db_session)

if case.dedicated_channel:
# we send a direct message to all participants asking them
# to rate and provide feedback about the case
send_case_rating_feedback_message(case, db_session)


def reactivate_case_participants(case: Case, db_session: Session):
"""Reactivates all case participants."""
Expand Down
42 changes: 42 additions & 0 deletions src/dispatch/case/messaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
CASE_TYPE_CHANGE,
CASE_SEVERITY_CHANGE,
CASE_PRIORITY_CHANGE,
CASE_CLOSED_RATING_FEEDBACK_NOTIFICATION,
MessageType,
)
from dispatch.config import DISPATCH_UI_URL
Expand Down Expand Up @@ -330,3 +331,44 @@ def send_case_welcome_participant_message(
)

log.debug(f"Welcome ephemeral message sent to {participant_email}.")


def send_case_rating_feedback_message(case: Case, db_session: Session):
"""
Sends a direct message to all case participants asking
them to rate and provide feedback about the case.
"""
notification_text = "Case Rating and Feedback"
notification_template = CASE_CLOSED_RATING_FEEDBACK_NOTIFICATION

plugin = plugin_service.get_active_instance(
db_session=db_session, project_id=case.project.id, plugin_type="conversation"
)
if not plugin:
log.warning("Case rating and feedback message not sent, no conversation plugin enabled.")
return

items = [
{
"case_id": case.id,
"organization_slug": case.project.organization.slug,
"name": case.name,
"title": case.title,
"ticket_weblink": case.ticket.weblink,
}
]

for participant in case.participants:
try:
plugin.instance.send_direct(
participant.individual.email,
notification_text,
notification_template,
MessageType.case_rating_feedback,
items=items,
)
except Exception as e:
# if one fails we don't want all to fail
log.exception(e)

log.debug("Case rating and feedback message sent to all participants.")
2 changes: 2 additions & 0 deletions src/dispatch/case/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ class Case(Base, TimeStampMixin, ProjectMixin):

events = relationship("Event", backref="case", cascade="all, delete-orphan")

feedback = relationship("Feedback", backref="case", cascade="all, delete-orphan")

groups = relationship(
"Group", backref="case", cascade="all, delete-orphan", foreign_keys=[Group.case_id]
)
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 @@ -14,6 +14,7 @@ class ConversationCommands(DispatchEnum):

class ConversationButtonActions(DispatchEnum):
feedback_notification_provide = "feedback-notification-provide"
case_feedback_notification_provide = "case-feedback-notification-provide"
invite_user = "invite-user"
invite_user_case = "invite-user-case"
monitor_link = "monitor-link"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Adds case_id and project_id to feedback
Revision ID: 3c49f62d7914
Revises: b8c1a8a4d957
Create Date: 2024-10-16 15:21:17.120891
"""

from alembic import op
import sqlalchemy as sa
from sqlalchemy.orm import Session
from dispatch.feedback.incident.models import Feedback

# revision identifiers, used by Alembic.
revision = "3c49f62d7914"
down_revision = "b8c1a8a4d957"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("feedback", sa.Column("case_id", sa.Integer(), nullable=True))
op.create_foreign_key(
"assoc_feedback_case_id_fkey",
"feedback",
"case",
["case_id"],
["id"],
ondelete="CASCADE",
)
op.add_column("feedback", sa.Column("project_id", sa.Integer(), nullable=True))
op.create_foreign_key(
"assoc_feedback_project_id_fkey",
"feedback",
"project",
["project_id"],
["id"],
ondelete="CASCADE",
)

bind = op.get_bind()
session = Session(bind=bind)

instances = session.query(Feedback).all()

for instance in instances:
if instance.incident:
instance.project_id = instance.incident.project_id
elif instance.case:
instance.project_id = instance.case.project_id

session.commit()
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint("assoc_feedback_case_id_fkey", "feedback", type_="foreignkey")
op.drop_column("feedback", "case_id")
op.drop_constraint("assoc_feedback_project_id_fkey", "feedback", type_="foreignkey")
op.drop_column("feedback", "project_id")
# ### end Alembic commands ###
7 changes: 3 additions & 4 deletions src/dispatch/database/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,8 @@ def build_filters(filter_spec):

if not _is_iterable_filter(fn_args):
raise BadFilterFormat(
"`{}` value must be an iterable across the function " "arguments".format(
boolean_function.key
)
"`{}` value must be an iterable across the function "
"arguments".format(boolean_function.key)
)
if boolean_function.only_one_arg and len(fn_args) != 1:
raise BadFilterFormat(
Expand Down Expand Up @@ -347,8 +346,8 @@ def apply_filter_specific_joins(model: Base, filter_spec: dict, query: orm.query
# this is required because by default sqlalchemy-filter's auto-join
# knows nothing about how to join many-many relationships.
model_map = {
(Feedback, "Project"): (Incident, False),
(Feedback, "Incident"): (Incident, False),
(Feedback, "Case"): (Case, False),
(Task, "Project"): (Incident, False),
(Task, "Incident"): (Incident, False),
(Task, "IncidentPriority"): (Incident, False),
Expand Down
19 changes: 12 additions & 7 deletions src/dispatch/feedback/incident/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,32 @@
from typing import Optional, List

from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy_utils import TSVectorType

from dispatch.database.core import Base
from dispatch.incident.models import IncidentReadMinimal
from dispatch.models import DispatchBase, TimeStampMixin, FeedbackMixin, PrimaryKey, Pagination
from dispatch.models import (
DispatchBase,
TimeStampMixin,
FeedbackMixin,
PrimaryKey,
Pagination,
ProjectMixin,
)
from dispatch.participant.models import ParticipantRead
from dispatch.project.models import ProjectRead
from dispatch.case.models import CaseReadMinimal

from .enums import FeedbackRating


class Feedback(TimeStampMixin, FeedbackMixin, Base):
class Feedback(TimeStampMixin, FeedbackMixin, ProjectMixin, Base):
# Columns
id = Column(Integer, primary_key=True)

# Relationships
incident_id = Column(Integer, ForeignKey("incident.id", ondelete="CASCADE"))
case_id = Column(Integer, ForeignKey("case.id", ondelete="CASCADE"))
participant_id = Column(Integer, ForeignKey("participant.id"))

search_vector = Column(
Expand All @@ -31,17 +39,14 @@ class Feedback(TimeStampMixin, FeedbackMixin, Base):
)
)

@hybrid_property
def project(self):
return self.incident.project


# Pydantic models
class FeedbackBase(DispatchBase):
created_at: Optional[datetime]
rating: FeedbackRating = FeedbackRating.very_satisfied
feedback: Optional[str] = Field(None, nullable=True)
incident: Optional[IncidentReadMinimal]
case: Optional[CaseReadMinimal]
participant: Optional[ParticipantRead]


Expand Down
23 changes: 18 additions & 5 deletions src/dispatch/feedback/incident/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from datetime import datetime, timedelta

from dispatch.incident import service as incident_service
from dispatch.case import service as case_service
from dispatch.incident.models import Incident
from dispatch.project.models import Project

Expand Down Expand Up @@ -34,13 +35,25 @@ def get_all_last_x_hours_by_project_id(

def create(*, db_session, feedback_in: FeedbackCreate) -> Feedback:
"""Creates a new piece of feedback."""
incident = incident_service.get(
db_session=db_session,
incident_id=feedback_in.incident.id,
)
if feedback_in.incident:
incident = incident_service.get(
db_session=db_session,
incident_id=feedback_in.incident.id,
)
project = incident.project
case = None
else:
case = case_service.get(
db_session=db_session,
case_id=feedback_in.case.id,
)
project = case.project
incident = None
feedback = Feedback(
**feedback_in.dict(exclude={"incident"}),
**feedback_in.dict(exclude={"incident", "case", "project"}),
incident=incident,
case=case,
project=project,
)
db_session.add(feedback)
db_session.commit()
Expand Down
19 changes: 19 additions & 0 deletions src/dispatch/messaging/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class MessageType(DispatchEnum):
case_status_reminder = "case-status-reminder"
service_feedback = "service-feedback"
task_add_to_incident = "task-add-to-incident"
case_rating_feedback = "case-rating-feedback"


INCIDENT_STATUS_DESCRIPTIONS = {
Expand Down Expand Up @@ -373,6 +374,9 @@ class MessageType(DispatchEnum):
INCIDENT_CLOSED_RATING_FEEDBACK_DESCRIPTION = """
Thanks for participating in the {{name}} ("{{title}}") incident. We would appreciate if you could rate your experience and provide feedback."""

CASE_CLOSED_RATING_FEEDBACK_DESCRIPTION = """
Thanks for participating in the {{name}} ("{{title}}") case. We would appreciate if you could rate your experience and provide feedback."""

INCIDENT_MANAGEMENT_HELP_TIPS_MESSAGE_DESCRIPTION = """
Hey, I see you're the Incident Commander for <{{conversation_weblink}}|{{name}}> ("{{title}}"). Here are a few things to consider when managing the incident:
\n • Keep the incident and its status up to date using the Slack `{{update_command}}` command.
Expand Down Expand Up @@ -971,6 +975,21 @@ class MessageType(DispatchEnum):
}
]

CASE_CLOSED_RATING_FEEDBACK_NOTIFICATION = [
{
"title": "{{name}} Case - Rating and Feedback",
"title_link": "{{ticket_weblink}}",
"text": CASE_CLOSED_RATING_FEEDBACK_DESCRIPTION,
"buttons": [
{
"button_text": "Provide Feedback",
"button_value": "{{organization_slug}}-{{case_id}}",
"button_action": ConversationButtonActions.case_feedback_notification_provide,
}
],
}
]

INCIDENT_FEEDBACK_DAILY_REPORT = [
{"title": "Incident", "text": "{{ name }}"},
{"title": "Incident Title", "text": "{{ title }}"},
Expand Down
17 changes: 17 additions & 0 deletions src/dispatch/plugins/dispatch_slack/feedback/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,34 @@ class IncidentFeedbackNotificationBlockIds(DispatchEnum):
anonymous_checkbox = "incident-feedback-notification-anonymous-checkbox"


class CaseFeedbackNotificationBlockIds(DispatchEnum):
feedback_input = "case-feedback-notification-feedback-input"
rating_select = "case-feedback-notification-rating-select"
anonymous_checkbox = "case-feedback-notification-anonymous-checkbox"


class IncidentFeedbackNotificationActionIds(DispatchEnum):
feedback_input = "incident-feedback-notification-feedback-input"
rating_select = "incident-feedback-notification-rating-select"
anonymous_checkbox = "incident-feedback-notification-anonymous-checkbox"


class CaseFeedbackNotificationActionIds(DispatchEnum):
feedback_input = "case-feedback-notification-feedback-input"
rating_select = "case-feedback-notification-rating-select"
anonymous_checkbox = "case-feedback-notification-anonymous-checkbox"


class IncidentFeedbackNotificationActions(DispatchEnum):
submit = "incident-feedback-notification-submit"
provide = ConversationButtonActions.feedback_notification_provide


class CaseFeedbackNotificationActions(DispatchEnum):
submit = "case-feedback-notification-submit"
provide = ConversationButtonActions.case_feedback_notification_provide


class ServiceFeedbackNotificationBlockIds(DispatchEnum):
anonymous_checkbox = "service-feedback-notification-anonymous-checkbox"
feedback_input = "service-feedback-notification-feedback-input"
Expand Down
Loading

0 comments on commit 1f34f83

Please sign in to comment.