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

feat(case): adding case feedback #5356

Merged
merged 9 commits into from
Oct 18, 2024
Merged
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
Loading