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 custom description to new incident Slack channels #4830

Merged
merged 9 commits into from
Jun 18, 2024
66 changes: 65 additions & 1 deletion src/dispatch/conversation/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from dispatch.plugin import service as plugin_service
from dispatch.storage.models import Storage
from dispatch.ticket.models import Ticket
from dispatch.service.models import Service
from dispatch.project.models import Project
from dispatch.utils import deslug_and_capitalize_resource_type
from dispatch.types import Subject

Expand Down Expand Up @@ -215,7 +217,7 @@ def get_topic_text(subject: Subject) -> str:
)


def set_conversation_topic(subject: Subject, db_session: SessionLocal):
def set_conversation_topic(subject: Subject, db_session: Session) -> None:
"""Sets the conversation topic."""
if not subject.conversation:
log.warning("Conversation topic not set. No conversation available for this incident/case.")
Expand All @@ -242,6 +244,68 @@ def set_conversation_topic(subject: Subject, db_session: SessionLocal):
log.exception(e)


def get_current_oncall_email(project: Project, service: Service, db_session: Session) -> str | None:
"""Notifies oncall about completed form"""
oncall_plugin = plugin_service.get_active_instance(
db_session=db_session, project_id=project.id, plugin_type="oncall"
)
if not oncall_plugin:
log.debug("Unable to send email since oncall plugin is not active.")
else:
return oncall_plugin.instance.get(service.external_id)


def get_description_text(subject: Subject, db_session: Session) -> str:
whitdog47 marked this conversation as resolved.
Show resolved Hide resolved
"""Returns the description details based on the subject"""
if not isinstance(subject, Incident):
return

incident_type = subject.incident_type
if not incident_type.channel_description:
return

description_service = incident_type.description_service
if description_service:
oncall_email = get_current_oncall_email(
project=subject.project,
service=description_service,
db_session=db_session
)
if oncall_email:
return incident_type.channel_description.replace("{oncall_email}", oncall_email)

return incident_type.channel_description


def set_conversation_description(subject: Subject, db_session: Session) -> None:
"""Sets the conversation description."""
if not subject.conversation:
log.warning("Conversation topic not set. No conversation available for this incident/case.")
return

plugin = plugin_service.get_active_instance(
db_session=db_session, project_id=subject.project.id, plugin_type="conversation"
)
if not plugin:
log.warning("Conversation topic not set. No conversation plugin enabled.")
return

conversation_description = get_description_text(subject, db_session)
if not conversation_description:
return

try:
plugin.instance.set_description(subject.conversation.channel_id, conversation_description)
except Exception as e:
event_service.log_subject_event(
subject=subject,
db_session=db_session,
source="Dispatch Core App",
description=f"Setting the incident/case conversation description failed. Reason: {e}",
)
log.exception(e)


def add_conversation_bookmark(
db_session: Session,
subject: Subject,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Create new channel description and description service id columns for incident type

Revision ID: 4286dcce0a2d
Revises: a836d4850a75
Create Date: 2024-06-12 17:45:25.556120

"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = "4286dcce0a2d"
down_revision = "a836d4850a75"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("incident_type", sa.Column("channel_description", sa.String(), nullable=True))
op.add_column("incident_type", sa.Column("description_service_id", sa.Integer(), nullable=True))
op.create_foreign_key("description_service_id_fkey", "incident_type", "service", ["description_service_id"], ["id"])
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint("description_service_id_fkey", "incident_type", type_="foreignkey")
op.drop_column("incident_type", "description_service_id")
op.drop_column("incident_type", "channel_description")
# ### end Alembic commands ###
4 changes: 4 additions & 0 deletions src/dispatch/incident/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,10 @@ def incident_create_resources(
# we set the conversation topic
conversation_flows.set_conversation_topic(incident, db_session)

# and set the conversation description
if incident.incident_type.channel_description is not None:
conversation_flows.set_conversation_description(incident, db_session)

# we set the conversation bookmarks
bookmarks = [
# resource, title
Expand Down
9 changes: 9 additions & 0 deletions src/dispatch/incident/type/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from dispatch.models import DispatchBase, ProjectMixin, Pagination
from dispatch.plugin.models import PluginMetadata
from dispatch.project.models import ProjectRead
from dispatch.service.models import ServiceRead


class IncidentType(ProjectMixin, Base):
Expand Down Expand Up @@ -64,6 +65,12 @@ class IncidentType(ProjectMixin, Base):
foreign_keys=[cost_model_id],
)

# Sets the channel description for the incidents of this type
channel_description = Column(String, nullable=True)
# Optionally add on-call name to the channel description
description_service_id = Column(Integer, ForeignKey("service.id"))
description_service = relationship("Service", foreign_keys=[description_service_id])

@hybrid_method
def get_meta(self, slug):
if not self.plugin_metadata:
Expand Down Expand Up @@ -101,6 +108,8 @@ class IncidentTypeBase(DispatchBase):
project: Optional[ProjectRead]
plugin_metadata: List[PluginMetadata] = []
cost_model: Optional[CostModelRead] = None
channel_description: Optional[str] = Field(None, nullable=True)
description_service: Optional[ServiceRead]

@validator("plugin_metadata", pre=True)
def replace_none_with_empty_list(cls, value):
Expand Down
17 changes: 17 additions & 0 deletions src/dispatch/incident/type/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from dispatch.document import service as document_service
from dispatch.exceptions import NotFoundError
from dispatch.project import service as project_service
from dispatch.service import service as service_service

from .models import IncidentType, IncidentTypeCreate, IncidentTypeRead, IncidentTypeUpdate

Expand Down Expand Up @@ -138,6 +139,7 @@ def create(*, db_session, incident_type_in: IncidentTypeCreate) -> IncidentType:
"review_template_document",
"cost_model",
"project",
"description_service",
}
),
project=project,
Expand Down Expand Up @@ -173,6 +175,13 @@ def create(*, db_session, incident_type_in: IncidentTypeCreate) -> IncidentType:
)
incident_type.tracking_template_document = tracking_template_document

if incident_type_in.description_service:
service = service_service.get(
db_session=db_session, service_id=incident_type_in.description_service.id
)
if service:
incident_type.description_service_id = service.id

db_session.add(incident_type)
db_session.commit()
return incident_type
Expand Down Expand Up @@ -226,6 +235,13 @@ def update(
)
incident_type.tracking_template_document = tracking_template_document

if incident_type_in.description_service:
service = service_service.get(
db_session=db_session, service_id=incident_type_in.description_service.id
)
if service:
incident_type.description_service_id = service.id

incident_type_data = incident_type.dict()

update_data = incident_type_in.dict(
Expand All @@ -236,6 +252,7 @@ def update(
"tracking_template_document",
"review_template_document",
"cost_model",
"description_service",
},
)

Expand Down
1 change: 1 addition & 0 deletions src/dispatch/plugins/dispatch_slack/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class SlackAPIPostEndpoints(DispatchEnum):
conversations_invite = "conversations.invite"
conversations_rename = "conversations.rename"
conversations_set_topic = "conversations.setTopic"
conversations_set_purpose = "conversations.setPurpose"
conversations_unarchive = "conversations.unarchive"
pins_add = "pins.add"

Expand Down
6 changes: 6 additions & 0 deletions src/dispatch/plugins/dispatch_slack/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
send_ephemeral_message,
send_message,
set_conversation_topic,
set_conversation_description,
unarchive_conversation,
update_message,
)
Expand Down Expand Up @@ -324,6 +325,11 @@ def set_topic(self, conversation_id: str, topic: str):
client = create_slack_client(self.configuration)
return set_conversation_topic(client, conversation_id, topic)

def set_description(self, conversation_id: str, description: str):
"""Sets the conversation description."""
client = create_slack_client(self.configuration)
return set_conversation_description(client, conversation_id, description)

def add_bookmark(self, conversation_id: str, weblink: str, title: str):
"""Adds a bookmark to the conversation."""
client = create_slack_client(self.configuration)
Expand Down
7 changes: 7 additions & 0 deletions src/dispatch/plugins/dispatch_slack/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,13 @@ def set_conversation_topic(client: WebClient, conversation_id: str, topic: str)
)


def set_conversation_description(client: WebClient, conversation_id: str, description: str) -> SlackResponse:
"""Sets the topic of the specified conversation."""
return make_call(
client, SlackAPIPostEndpoints.conversations_set_purpose, channel=conversation_id, purpose=description
)


def add_conversation_bookmark(
client: WebClient, conversation_id: str, weblink: str, title: str
) -> SlackResponse:
Expand Down
23 changes: 23 additions & 0 deletions src/dispatch/static/dispatch/src/incident/type/NewEditSheet.vue
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,25 @@
hint="Determines whether this incident type is availible for new incidents."
/>
</v-col>
<v-col cols="12">
<v-textarea
v-model="channel_description"
label="Channel description"
hint="Set this as the description for new incident channels."
clearable
/>
</v-col>
<v-col cols="12">
<span class="text-body-1 text-medium-emphasis mt-2">
Use {oncall_email} in the description to replace with this oncall email
(optional).
</span>
<service-select
label="Oncall Service"
:project="project"
v-model="description_service"
/>
</v-col>
<v-col cols="12">
<plugin-metadata-input v-model="plugin_metadata" :project="project" />
</v-col>
Expand All @@ -152,6 +171,7 @@ import { mapFields } from "vuex-map-fields"
import CostModelCombobox from "@/cost_model/CostModelCombobox.vue"
import PluginMetadataInput from "@/plugin/PluginMetadataInput.vue"
import TemplateSelect from "@/document/template/TemplateSelect.vue"
import ServiceSelect from "@/service/ServiceSelect.vue"

export default {
setup() {
Expand All @@ -165,6 +185,7 @@ export default {
CostModelCombobox,
PluginMetadataInput,
TemplateSelect,
ServiceSelect,
},

data() {
Expand Down Expand Up @@ -194,6 +215,8 @@ export default {
"selected.cost_model",
"selected.exclude_from_metrics",
"selected.default",
"selected.channel_description",
"selected.description_service",
]),
...mapFields("incident_type", {
default_incident_type: "selected.default",
Expand Down
Loading