diff --git a/src/dispatch/conversation/flows.py b/src/dispatch/conversation/flows.py
index 8846fc320f7b..c0923ba30ce1 100644
--- a/src/dispatch/conversation/flows.py
+++ b/src/dispatch/conversation/flows.py
@@ -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
@@ -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.")
@@ -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 | None:
+ """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,
diff --git a/src/dispatch/database/revisions/tenant/versions/2024-06-12_4286dcce0a2d.py b/src/dispatch/database/revisions/tenant/versions/2024-06-12_4286dcce0a2d.py
new file mode 100644
index 000000000000..41ed3ce919b3
--- /dev/null
+++ b/src/dispatch/database/revisions/tenant/versions/2024-06-12_4286dcce0a2d.py
@@ -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 ###
diff --git a/src/dispatch/incident/flows.py b/src/dispatch/incident/flows.py
index 6b7c594ab46c..90c15a462314 100644
--- a/src/dispatch/incident/flows.py
+++ b/src/dispatch/incident/flows.py
@@ -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
diff --git a/src/dispatch/incident/type/models.py b/src/dispatch/incident/type/models.py
index 64ba3f2e4d15..fb98ec79732b 100644
--- a/src/dispatch/incident/type/models.py
+++ b/src/dispatch/incident/type/models.py
@@ -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):
@@ -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:
@@ -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):
diff --git a/src/dispatch/incident/type/service.py b/src/dispatch/incident/type/service.py
index f1eabc3dedd4..6847a03548aa 100644
--- a/src/dispatch/incident/type/service.py
+++ b/src/dispatch/incident/type/service.py
@@ -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
@@ -138,6 +139,7 @@ def create(*, db_session, incident_type_in: IncidentTypeCreate) -> IncidentType:
"review_template_document",
"cost_model",
"project",
+ "description_service",
}
),
project=project,
@@ -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
@@ -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(
@@ -236,6 +252,7 @@ def update(
"tracking_template_document",
"review_template_document",
"cost_model",
+ "description_service",
},
)
diff --git a/src/dispatch/plugins/dispatch_slack/enums.py b/src/dispatch/plugins/dispatch_slack/enums.py
index b78a24c7f50e..095b829334c0 100644
--- a/src/dispatch/plugins/dispatch_slack/enums.py
+++ b/src/dispatch/plugins/dispatch_slack/enums.py
@@ -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"
diff --git a/src/dispatch/plugins/dispatch_slack/plugin.py b/src/dispatch/plugins/dispatch_slack/plugin.py
index 687f48f05cda..fcceb6b2ba8c 100644
--- a/src/dispatch/plugins/dispatch_slack/plugin.py
+++ b/src/dispatch/plugins/dispatch_slack/plugin.py
@@ -58,6 +58,7 @@
send_ephemeral_message,
send_message,
set_conversation_topic,
+ set_conversation_description,
unarchive_conversation,
update_message,
)
@@ -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)
diff --git a/src/dispatch/plugins/dispatch_slack/service.py b/src/dispatch/plugins/dispatch_slack/service.py
index 0dc9fce8eed1..3c8e9089815e 100644
--- a/src/dispatch/plugins/dispatch_slack/service.py
+++ b/src/dispatch/plugins/dispatch_slack/service.py
@@ -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:
diff --git a/src/dispatch/static/dispatch/src/incident/type/NewEditSheet.vue b/src/dispatch/static/dispatch/src/incident/type/NewEditSheet.vue
index 5652339d3ad8..bd7dfedffd96 100644
--- a/src/dispatch/static/dispatch/src/incident/type/NewEditSheet.vue
+++ b/src/dispatch/static/dispatch/src/incident/type/NewEditSheet.vue
@@ -132,6 +132,25 @@
hint="Determines whether this incident type is availible for new incidents."
/>
+
+
+
+
+
+ Use {oncall_email} in the description to replace with this oncall email
+ (optional).
+
+
+
@@ -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() {
@@ -165,6 +185,7 @@ export default {
CostModelCombobox,
PluginMetadataInput,
TemplateSelect,
+ ServiceSelect,
},
data() {
@@ -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",