From b2cd9b5466be8826e19b0150b94f56998e37d43f Mon Sep 17 00:00:00 2001
From: David Whittaker <84562015+whitdog47@users.noreply.github.com>
Date: Mon, 14 Oct 2024 15:45:40 -0700
Subject: [PATCH] feat(ui/ux): adds a task edit dialog with create ticket
action (#5260)
---
.../versions/2024-10-09_b8c1a8a4d957.py | 35 +++++
src/dispatch/incident/models.py | 4 +
src/dispatch/incident/type/models.py | 11 ++
src/dispatch/plugins/dispatch_core/plugin.py | 16 +++
src/dispatch/plugins/dispatch_jira/plugin.py | 75 +++++++++++
.../dispatch/src/incident/TaskEditDialog.vue | 127 ++++++++++++++++++
.../static/dispatch/src/incident/TasksTab.vue | 99 ++++++++++++--
.../static/dispatch/src/incident/store.js | 48 +++++++
.../src/incident/type/NewEditSheet.vue | 12 +-
.../src/plugin/PluginInstanceCombobox.vue | 10 +-
.../static/dispatch/src/task/NewEditSheet.vue | 10 +-
.../static/dispatch/src/task/Table.vue | 2 +-
src/dispatch/static/dispatch/src/task/api.js | 4 +
src/dispatch/task/models.py | 3 +
src/dispatch/task/service.py | 19 +++
src/dispatch/task/views.py | 17 +++
src/dispatch/ticket/flows.py | 79 ++++++++++-
src/dispatch/ticket/models.py | 1 +
18 files changed, 541 insertions(+), 31 deletions(-)
create mode 100644 src/dispatch/database/revisions/tenant/versions/2024-10-09_b8c1a8a4d957.py
create mode 100644 src/dispatch/static/dispatch/src/incident/TaskEditDialog.vue
diff --git a/src/dispatch/database/revisions/tenant/versions/2024-10-09_b8c1a8a4d957.py b/src/dispatch/database/revisions/tenant/versions/2024-10-09_b8c1a8a4d957.py
new file mode 100644
index 000000000000..103bffd67bfd
--- /dev/null
+++ b/src/dispatch/database/revisions/tenant/versions/2024-10-09_b8c1a8a4d957.py
@@ -0,0 +1,35 @@
+"""Adds tickets to tasks and ticket metadata to incident types
+
+Revision ID: b8c1a8a4d957
+Revises: b057c079c2d5
+Create Date: 2024-10-05 09:06:34.177407
+
+"""
+
+from alembic import op
+import sqlalchemy as sa
+
+# revision identifiers, used by Alembic.
+revision = "b8c1a8a4d957"
+down_revision = "b057c079c2d5"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column(
+ "incident_type",
+ sa.Column("task_plugin_metadata", sa.JSON(), nullable=True, server_default="[]"),
+ )
+ op.add_column("ticket", sa.Column("task_id", sa.Integer(), nullable=True))
+ op.create_foreign_key(None, "ticket", "task", ["task_id"], ["id"], ondelete="CASCADE")
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_constraint(None, "ticket", type_="foreignkey")
+ op.drop_column("ticket", "task_id")
+ op.drop_column("incident_type", "task_plugin_metadata")
+ # ### end Alembic commands ###
diff --git a/src/dispatch/incident/models.py b/src/dispatch/incident/models.py
index bfd0ed554f8b..5d98f383768e 100644
--- a/src/dispatch/incident/models.py
+++ b/src/dispatch/incident/models.py
@@ -253,7 +253,11 @@ class TaskRead(DispatchBase):
created_at: Optional[datetime]
description: Optional[str] = Field(None, nullable=True)
status: TaskStatus = TaskStatus.open
+ owner: Optional[ParticipantRead]
weblink: Optional[AnyHttpUrl] = Field(None, nullable=True)
+ resolve_by: Optional[datetime]
+ resolved_at: Optional[datetime]
+ ticket: Optional[TicketRead] = None
class TaskReadMinimal(DispatchBase):
diff --git a/src/dispatch/incident/type/models.py b/src/dispatch/incident/type/models.py
index fb98ec79732b..6fde577647a7 100644
--- a/src/dispatch/incident/type/models.py
+++ b/src/dispatch/incident/type/models.py
@@ -31,6 +31,7 @@ class IncidentType(ProjectMixin, Base):
default = Column(Boolean, default=False)
visibility = Column(String, default=Visibility.open)
plugin_metadata = Column(JSON, default=[])
+ task_plugin_metadata = Column(JSON, default=[])
incident_template_document_id = Column(Integer, ForeignKey("document.id"))
incident_template_document = relationship(
@@ -80,6 +81,15 @@ def get_meta(self, slug):
if m["slug"] == slug:
return m
+ @hybrid_method
+ def get_task_meta(self, slug):
+ if not self.task_plugin_metadata:
+ return
+
+ for m in self.task_plugin_metadata:
+ if m["slug"] == slug:
+ return m
+
listen(IncidentType.default, "set", ensure_unique_default_per_project)
@@ -110,6 +120,7 @@ class IncidentTypeBase(DispatchBase):
cost_model: Optional[CostModelRead] = None
channel_description: Optional[str] = Field(None, nullable=True)
description_service: Optional[ServiceRead]
+ task_plugin_metadata: List[PluginMetadata] = []
@validator("plugin_metadata", pre=True)
def replace_none_with_empty_list(cls, value):
diff --git a/src/dispatch/plugins/dispatch_core/plugin.py b/src/dispatch/plugins/dispatch_core/plugin.py
index 073b65738a0f..84593ce87b65 100644
--- a/src/dispatch/plugins/dispatch_core/plugin.py
+++ b/src/dispatch/plugins/dispatch_core/plugin.py
@@ -263,6 +263,22 @@ def update_case_ticket(
"""Updates a Dispatch case ticket."""
return
+ def create_task_ticket(
+ self,
+ task_id: int,
+ title: str,
+ assignee_email: str,
+ reporter_email: str,
+ incident_ticket_key: str = None,
+ task_plugin_metadata: dict = None,
+ db_session=None,
+ ):
+ """Creates a Dispatch task ticket."""
+ return {
+ "resource_id": "",
+ "weblink": "https://dispatch.example.com",
+ }
+
class DispatchDocumentResolverPlugin(DocumentResolverPlugin):
title = "Dispatch Plugin - Document Resolver"
diff --git a/src/dispatch/plugins/dispatch_jira/plugin.py b/src/dispatch/plugins/dispatch_jira/plugin.py
index 4b6ed634a3c4..42cde3f13a0b 100644
--- a/src/dispatch/plugins/dispatch_jira/plugin.py
+++ b/src/dispatch/plugins/dispatch_jira/plugin.py
@@ -123,6 +123,17 @@ def process_plugin_metadata(plugin_metadata: dict):
return project_id, issue_type_name
+def create_dict_from_plugin_metadata(plugin_metadata: dict):
+ """Creates a dictionary from plugin metadata, excluding project_id and issue_type_name."""
+ metadata_dict = {}
+ if plugin_metadata:
+ for key_value in plugin_metadata["metadata"]:
+ if key_value["key"] != "project_id" and key_value["key"] != "issue_type_name":
+ metadata_dict[key_value["key"]] = key_value["value"]
+
+ return metadata_dict
+
+
def create_client(configuration: JiraConfiguration) -> JIRA:
"""Creates a Jira client."""
return JIRA(
@@ -392,6 +403,70 @@ def create_case_ticket(
return create(self.configuration, client, issue_fields)
+ def create_task_ticket(
+ self,
+ task_id: int,
+ title: str,
+ assignee_email: str,
+ reporter_email: str,
+ incident_ticket_key: str = None,
+ task_plugin_metadata: dict = None,
+ db_session=None,
+ ):
+ """Creates a task Jira issue."""
+ client = create_client(self.configuration)
+
+ assignee = get_user_field(client, self.configuration, assignee_email)
+ reporter = get_user_field(client, self.configuration, reporter_email)
+
+ project_id, issue_type_name = process_plugin_metadata(task_plugin_metadata)
+ other_fields = create_dict_from_plugin_metadata(task_plugin_metadata)
+
+ if not project_id:
+ project_id = self.configuration.default_project_id
+
+ project = {"id": project_id}
+ if not project_id.isdigit():
+ project = {"key": project_id}
+
+ if not issue_type_name:
+ issue_type_name = self.configuration.default_issue_type_name
+
+ issuetype = {"name": issue_type_name}
+
+ issue_fields = {
+ "project": project,
+ "issuetype": issuetype,
+ "assignee": assignee,
+ "reporter": reporter,
+ "summary": title,
+ **other_fields,
+ }
+
+ issue = client.create_issue(fields=issue_fields)
+
+ if incident_ticket_key:
+ update = {
+ "issuelinks": [
+ {
+ "add": {
+ "type": {
+ "name": "Relates",
+ "inward": "is related to",
+ "outward": "relates to",
+ },
+ "outwardIssue": {"key": incident_ticket_key},
+ }
+ }
+ ]
+ }
+ issue.update(update=update)
+
+ return {
+ "resource_id": issue.key,
+ "weblink": f"{self.configuration.browser_url}/browse/{issue.key}",
+ }
+
def update_case_ticket(
self,
ticket_id: str,
diff --git a/src/dispatch/static/dispatch/src/incident/TaskEditDialog.vue b/src/dispatch/static/dispatch/src/incident/TaskEditDialog.vue
new file mode 100644
index 000000000000..84513a9ae054
--- /dev/null
+++ b/src/dispatch/static/dispatch/src/incident/TaskEditDialog.vue
@@ -0,0 +1,127 @@
+
+
+
+
+
+ Edit Task
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+ OK
+
+
+
+
+
+
+
+
diff --git a/src/dispatch/static/dispatch/src/incident/TasksTab.vue b/src/dispatch/static/dispatch/src/incident/TasksTab.vue
index 2da5d784f257..44174f20790d 100644
--- a/src/dispatch/static/dispatch/src/incident/TasksTab.vue
+++ b/src/dispatch/static/dispatch/src/incident/TasksTab.vue
@@ -1,19 +1,90 @@
+
-
-
-
+ Open tasks
+
+
+
{{ task.description }}
- Created: {{ formatRelativeDate(task.created_at) }} |
- Status: {{ task.status }} | Assignees:
- {{ individualNames(task.assignees) }}
+ Created: {{ formatRelativeDate(task.created_at) }} |
+ Status: {{ task.status }} | Assignee:
+ {{ individualNames(task.assignees) }} | Due:
+ {{ formatRelativeDate(task.resolve_by) }}
+
-
- mdi-open-in-new
+
+
+
+ mdi-dots-vertical
+
+
+
+
+ View / Edit
+
+
+ Go to task
+
+
+ Go to ticket
+
+
+ Create ticket
+
+
+
+
+
+
+
+ Resolved tasks
+
+
+
+ {{ task.description }}
+
+
+ Created: {{ formatRelativeDate(task.created_at) }} |
+ Status: {{ task.status }} | Assignee:
+ {{ individualNames(task.assignees) }} | Resolved:
+ {{ formatRelativeDate(task.resolved_at) }}
+
+
+
+
+
+
+ mdi-dots-vertical
+
+
+
+
+ View / Edit
+
+
+ Go to task
+
+
+ Create ticket
+
+
+
@@ -27,17 +98,27 @@
diff --git a/src/dispatch/static/dispatch/src/incident/store.js b/src/dispatch/static/dispatch/src/incident/store.js
index 9045f5f3d3d9..226b954d8833 100644
--- a/src/dispatch/static/dispatch/src/incident/store.js
+++ b/src/dispatch/static/dispatch/src/incident/store.js
@@ -6,9 +6,13 @@ import IncidentApi from "@/incident/api"
import ProjectApi from "@/project/api"
import PluginApi from "@/plugin/api"
import AuthApi from "@/auth/api"
+import TaskApi from "@/task/api"
+
import router from "@/router"
import moment from "moment-timezone"
+import { cloneDeep } from "lodash"
+
const getDefaultSelectedState = () => {
return {
cases: [],
@@ -74,6 +78,7 @@ const state = {
showNewSheet: false,
showReportDialog: false,
showEditEventDialog: false,
+ showEditTaskDialog: false,
showDeleteEventDialog: false,
},
report: {
@@ -277,6 +282,32 @@ const actions = {
state.selected.currentEvent = { started_at, description: "", uuid: "" }
commit("SET_DIALOG_EDIT_EVENT", true)
},
+ showNewTaskDialog({ commit }, task) {
+ state.selected.currentTask = task
+ commit("SET_DIALOG_EDIT_TASK", true)
+ },
+ createTicket({ commit }, task) {
+ TaskApi.createTicket(task.id).then((response) => {
+ const ticket = response.data
+ if (ticket) {
+ task.ticket = ticket
+ commit(
+ "notification_backend/addBeNotification",
+ { text: "Ticket created successfully.", type: "success" },
+ { root: true }
+ )
+ } else {
+ commit(
+ "notification_backend/addBeNotification",
+ { text: "Ticket creation failed.", type: "error" },
+ { root: true }
+ )
+ }
+ })
+ },
+ closeNewTaskDialog({ commit }) {
+ commit("SET_DIALOG_EDIT_TASK", false)
+ },
showDeleteEventDialog({ commit }, event) {
state.selected.currentEvent = event
commit("SET_DIALOG_DELETE_EVENT", true)
@@ -356,6 +387,20 @@ const actions = {
})
commit("SET_DIALOG_DELETE_EVENT", false)
},
+ updateExistingTask({ commit }) {
+ if (Array.isArray(state.selected.currentTask.owner)) {
+ state.selected.currentTask.owner = state.selected.currentTask.owner[0]
+ }
+ state.selected.currentTask.incident = cloneDeep(state.selected)
+ TaskApi.update(state.selected.currentTask.id, state.selected.currentTask).then(() => {
+ commit(
+ "notification_backend/addBeNotification",
+ { text: "Task updated successfully.", type: "success" },
+ { root: true }
+ )
+ })
+ commit("SET_DIALOG_EDIT_TASK", false)
+ },
report({ commit, dispatch }) {
commit("SET_SELECTED_LOADING", true)
return IncidentApi.create(state.selected)
@@ -606,6 +651,9 @@ const mutations = {
SET_DIALOG_DELETE_EVENT(state, value) {
state.dialogs.showDeleteEventDialog = value
},
+ SET_DIALOG_EDIT_TASK(state, value) {
+ state.dialogs.showEditTaskDialog = value
+ },
SET_DIALOG_REPORT(state, value) {
state.dialogs.showReportDialog = value
},
diff --git a/src/dispatch/static/dispatch/src/incident/type/NewEditSheet.vue b/src/dispatch/static/dispatch/src/incident/type/NewEditSheet.vue
index 35e5cb251b15..6884374512c6 100644
--- a/src/dispatch/static/dispatch/src/incident/type/NewEditSheet.vue
+++ b/src/dispatch/static/dispatch/src/incident/type/NewEditSheet.vue
@@ -151,8 +151,17 @@
v-model="description_service"
/>
+ Incident ticket plugin
-
+
+
+ Task ticket plugin
+
+
@@ -217,6 +226,7 @@ export default {
"selected.default",
"selected.channel_description",
"selected.description_service",
+ "selected.task_plugin_metadata",
]),
...mapFields("incident_type", {
default_incident_type: "selected.default",
diff --git a/src/dispatch/static/dispatch/src/plugin/PluginInstanceCombobox.vue b/src/dispatch/static/dispatch/src/plugin/PluginInstanceCombobox.vue
index 9d8319f64b46..ae36f13bd6a9 100644
--- a/src/dispatch/static/dispatch/src/plugin/PluginInstanceCombobox.vue
+++ b/src/dispatch/static/dispatch/src/plugin/PluginInstanceCombobox.vue
@@ -141,15 +141,7 @@ export default {
filter: JSON.stringify(filter),
}
- if (this.project) {
- filterOptions = {
- ...filterOptions,
- filters: {
- project: [this.project],
- },
- }
- filterOptions = SearchUtils.createParametersFromTableOptions({ ...filterOptions })
- }
+ filterOptions = SearchUtils.createParametersFromTableOptions({ ...filterOptions })
PluginApi.getAllInstances(filterOptions).then((response) => {
this.items = response.data.items
diff --git a/src/dispatch/static/dispatch/src/task/NewEditSheet.vue b/src/dispatch/static/dispatch/src/task/NewEditSheet.vue
index fff63a0af0d6..28912344db4f 100644
--- a/src/dispatch/static/dispatch/src/task/NewEditSheet.vue
+++ b/src/dispatch/static/dispatch/src/task/NewEditSheet.vue
@@ -46,7 +46,7 @@
v-model="status"
label="Status"
:items="statuses"
- hint="The incident's current status"
+ hint="The task's current status"
/>
@@ -64,7 +64,7 @@
diff --git a/src/dispatch/static/dispatch/src/task/Table.vue b/src/dispatch/static/dispatch/src/task/Table.vue
index b6fa842a9dbc..ac943ea1791a 100644
--- a/src/dispatch/static/dispatch/src/task/Table.vue
+++ b/src/dispatch/static/dispatch/src/task/Table.vue
@@ -157,7 +157,7 @@ export default {
{ title: "Status", value: "status", sortable: true },
{ title: "Creator", value: "creator.individual_contact.name", sortable: false },
{ title: "Owner", value: "owner.individual_contact.name", sortable: false },
- { title: "Assignees", value: "assignees", sortable: false },
+ { title: "Assignee", value: "assignees", sortable: false },
{ title: "Description", value: "description", sortable: false },
{ title: "Source", value: "source", sortable: true },
{ title: "Project", value: "project.name", sortable: false },
diff --git a/src/dispatch/static/dispatch/src/task/api.js b/src/dispatch/static/dispatch/src/task/api.js
index 40e3448260e8..d033865f8e66 100644
--- a/src/dispatch/static/dispatch/src/task/api.js
+++ b/src/dispatch/static/dispatch/src/task/api.js
@@ -30,4 +30,8 @@ export default {
delete(taskId) {
return API.delete(`${resource}/${taskId}`)
},
+
+ createTicket(taskId) {
+ return API.post(`${resource}/ticket/${taskId}`)
+ },
}
diff --git a/src/dispatch/task/models.py b/src/dispatch/task/models.py
index e5798204075c..4d92ba7e514c 100644
--- a/src/dispatch/task/models.py
+++ b/src/dispatch/task/models.py
@@ -23,6 +23,7 @@
from dispatch.models import ResourceBase, ResourceMixin, PrimaryKey, Pagination
from dispatch.participant.models import ParticipantRead, ParticipantUpdate
from dispatch.project.models import ProjectRead
+from dispatch.ticket.models import TicketRead
from .enums import TaskSource, TaskStatus, TaskPriority
@@ -65,6 +66,7 @@ class Task(Base, ResourceMixin):
priority = Column(String, default=TaskPriority.low)
status = Column(String, default=TaskStatus.open)
reminders = Column(Boolean, default=True)
+ ticket = relationship("Ticket", uselist=False, backref="task", cascade="all, delete-orphan")
search_vector = Column(
TSVectorType(
@@ -121,6 +123,7 @@ class TaskUpdate(TaskBase):
class TaskRead(TaskBase):
id: PrimaryKey
project: Optional[ProjectRead]
+ ticket: Optional[TicketRead] = None
class TaskPagination(Pagination):
diff --git a/src/dispatch/task/service.py b/src/dispatch/task/service.py
index 7ca2b24ad083..cb2d43b6babf 100644
--- a/src/dispatch/task/service.py
+++ b/src/dispatch/task/service.py
@@ -188,6 +188,25 @@ def update(*, db_session, task: Task, task_in: TaskUpdate, sync_external: bool =
for field in update_data.keys():
setattr(task, field, update_data[field])
+ if task_in.owner:
+ task.owner = participant_service.get_by_incident_id_and_email(
+ db_session=db_session,
+ incident_id=task.incident.id,
+ email=task_in.owner.individual.email,
+ )
+
+ if task_in.assignees:
+ assignees = []
+ for i in task_in.assignees:
+ assignees.append(
+ participant_service.get_by_incident_id_and_email(
+ db_session=db_session,
+ incident_id=task.incident.id,
+ email=i.individual.email,
+ )
+ )
+ task.assignees = assignees
+
# if we have an external task plugin enabled, attempt to update the external resource as well
# we don't currently have a good way to get the correct file_id (we don't store a task <-> relationship)
# lets try in both the incident doc and PIR doc
diff --git a/src/dispatch/task/views.py b/src/dispatch/task/views.py
index 9973dcc3893b..fb09aff989c6 100644
--- a/src/dispatch/task/views.py
+++ b/src/dispatch/task/views.py
@@ -8,8 +8,10 @@
from dispatch.database.core import DbSession
from dispatch.database.service import CommonParameters, search_filter_sort_paginate
from dispatch.models import PrimaryKey
+
from .enums import TaskStatus
from .flows import send_task_notification
+from dispatch.ticket.flows import create_task_ticket
from dispatch.messaging.strings import (
INCIDENT_TASK_NEW_NOTIFICATION,
INCIDENT_TASK_RESOLVED_NOTIFICATION,
@@ -63,6 +65,21 @@ def create_task(
return task
+@router.post("/ticket/{task_id}", tags=["tasks"])
+def create_ticket(
+ db_session: DbSession,
+ task_id: PrimaryKey,
+):
+ """Creates a ticket for an existing task."""
+ task = get(db_session=db_session, task_id=task_id)
+ if not task:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail=[{"msg": "A task with this id does not exist."}],
+ )
+ return create_task_ticket(task=task, db_session=db_session)
+
+
@router.put("/{task_id}", response_model=TaskRead, tags=["tasks"])
def update_task(db_session: DbSession, task_id: PrimaryKey, task_in: TaskUpdate):
"""Updates an existing task."""
diff --git a/src/dispatch/ticket/flows.py b/src/dispatch/ticket/flows.py
index 94a96313d489..9b6c045e8594 100644
--- a/src/dispatch/ticket/flows.py
+++ b/src/dispatch/ticket/flows.py
@@ -1,14 +1,18 @@
import logging
+from sqlalchemy.orm import Session
+
from dispatch.case.models import Case
from dispatch.case.type import service as case_type_service
-from dispatch.database.core import SessionLocal, resolve_attr
+from dispatch.database.core import resolve_attr
from dispatch.enums import Visibility
from dispatch.event import service as event_service
from dispatch.incident import service as incident_service
from dispatch.incident.models import Incident
from dispatch.incident.type import service as incident_type_service
+from dispatch.participant import service as participant_service
from dispatch.plugin import service as plugin_service
+from dispatch.task.models import Task
from .models import Ticket, TicketCreate
from .service import create
@@ -17,7 +21,7 @@
log = logging.getLogger(__name__)
-def create_incident_ticket(incident: Incident, db_session: SessionLocal):
+def create_incident_ticket(incident: Incident, db_session: Session):
"""Creates a ticket for an incident."""
plugin = plugin_service.get_active_instance(
db_session=db_session, project_id=incident.project.id, plugin_type="ticket"
@@ -77,7 +81,7 @@ def create_incident_ticket(incident: Incident, db_session: SessionLocal):
def update_incident_ticket(
incident_id: int,
- db_session: SessionLocal,
+ db_session: Session,
):
"""Updates an incident ticket."""
incident = incident_service.get(db_session=db_session, incident_id=incident_id)
@@ -135,7 +139,7 @@ def update_incident_ticket(
)
-def create_case_ticket(case: Case, db_session: SessionLocal):
+def create_case_ticket(case: Case, db_session: Session):
"""Creates a ticket for a case."""
plugin = plugin_service.get_active_instance(
db_session=db_session, project_id=case.project.id, plugin_type="ticket"
@@ -194,7 +198,7 @@ def create_case_ticket(case: Case, db_session: SessionLocal):
def update_case_ticket(
case: Case,
- db_session: SessionLocal,
+ db_session: Session,
):
"""Updates a case ticket."""
plugin = plugin_service.get_active_instance(
@@ -251,7 +255,7 @@ def update_case_ticket(
)
-def delete_ticket(ticket: Ticket, project_id: int, db_session: SessionLocal):
+def delete_ticket(ticket: Ticket, project_id: int, db_session: Session):
"""Deletes a ticket."""
plugin = plugin_service.get_active_instance(
db_session=db_session, project_id=project_id, plugin_type="ticket"
@@ -263,3 +267,66 @@ def delete_ticket(ticket: Ticket, project_id: int, db_session: SessionLocal):
log.exception(e)
else:
log.warning("Ticket not deleted. No ticket plugin enabled.")
+
+
+def create_task_ticket(task: Task, db_session: Session):
+ """Creates a ticket for an incident."""
+ plugin = plugin_service.get_active_instance(
+ db_session=db_session, project_id=task.project.id, plugin_type="ticket"
+ )
+ if not plugin:
+ log.warning("Task ticket not created. No ticket plugin enabled.")
+ return
+
+ title = task.description
+
+ incident = incident_service.get(db_session=db_session, incident_id=task.incident_id)
+ if not incident:
+ log.error(f"Task ticket not created. No incident associated with task {task.id}.")
+ return
+
+ owner = participant_service.get(db_session=db_session, participant_id=task.owner_id)
+ if not owner:
+ log.error(f"Task ticket not created. No owner associated with task {task.id}.")
+ return
+
+ if not task.assignees:
+ log.error(f"Task ticket not created. No assignees associated with task {task.id}.")
+ return
+
+ task_plugin_metadata = incident_type_service.get_by_name_or_raise(
+ db_session=db_session,
+ project_id=task.project.id,
+ incident_type_in=incident.incident_type,
+ ).get_task_meta(plugin.plugin.slug)
+
+ # we create the external task ticket
+ try:
+ external_ticket = plugin.instance.create_task_ticket(
+ task_id=task.id,
+ title=title,
+ assignee_email=task.assignees[0].individual.email,
+ reporter_email=owner.individual.email,
+ incident_ticket_key=incident.ticket.resource_id,
+ task_plugin_metadata=task_plugin_metadata,
+ db_session=db_session,
+ )
+ except Exception as e:
+ log.exception(e)
+ return
+
+ if not external_ticket:
+ log.error(f"Task ticket not created. Plugin {plugin.plugin.slug} encountered an error.")
+ return
+
+ external_ticket.update({"resource_type": plugin.plugin.slug})
+
+ # we create the internal task ticket
+ ticket_in = TicketCreate(**external_ticket)
+ ticket = create(db_session=db_session, ticket_in=ticket_in)
+ task.ticket = ticket
+
+ db_session.add(task)
+ db_session.commit()
+
+ return external_ticket
diff --git a/src/dispatch/ticket/models.py b/src/dispatch/ticket/models.py
index 6ce93e912513..7e83ab1a3065 100644
--- a/src/dispatch/ticket/models.py
+++ b/src/dispatch/ticket/models.py
@@ -13,6 +13,7 @@ class Ticket(Base, ResourceMixin):
id = Column(Integer, primary_key=True)
incident_id = Column(Integer, ForeignKey("incident.id", ondelete="CASCADE"))
case_id = Column(Integer, ForeignKey("case.id", ondelete="CASCADE"))
+ task_id = Column(Integer, ForeignKey("task.id", ondelete="CASCADE"))
# Pydantic models...