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",