Skip to content
This repository was archived by the owner on Sep 3, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/dispatch/event/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ def log_incident_event(
event_service.log_incident_event(
db_session=db_session,
incident_id=incident_id,
individual_id=individual.id,
owner=individual.name,
**event_in.__dict__,
)

Expand All @@ -53,3 +55,18 @@ def delete_incident_event(
db_session=db_session,
uuid=event_uuid,
)


@background_task
def export_timeline(
timeline_filters: dict,
incident_id: int,
db_session=None,
organization_slug: str = None,
):
status = event_service.export_timeline(
db_session=db_session,
timeline_filters=timeline_filters,
incident_id=incident_id,
)
return status
243 changes: 241 additions & 2 deletions src/dispatch/event/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from uuid import uuid4
import datetime
import logging
import json
import pytz

from dispatch.auth import service as auth_service
from dispatch.case import service as case_service
Expand All @@ -10,14 +12,20 @@
from dispatch.enums import EventType

from .models import Event, EventCreate, EventUpdate

from dispatch.document import service as document_service
from dispatch.plugin import service as plugin_service

log = logging.getLogger(__name__)


def get(*, db_session, event_id: int) -> Optional[Event]:
"""Get an event by id."""
return db_session.query(Event).filter(Event.id == event_id).one_or_none()
return (
db_session.query(Event)
.filter(Event.id == event_id)
.order_by(Event.started_at)
.one_or_none()
)


def get_by_case_id(*, db_session, case_id: int) -> list[Event | None]:
Expand All @@ -27,6 +35,7 @@ def get_by_case_id(*, db_session, case_id: int) -> list[Event | None]:

def get_by_incident_id(*, db_session, incident_id: int) -> list[Event | None]:
"""Get events by incident id."""

return db_session.query(Event).filter(Event.incident_id == incident_id)


Expand Down Expand Up @@ -182,3 +191,233 @@ def delete_incident_event(
event = get_by_uuid(db_session=db_session, uuid=uuid)

delete(db_session=db_session, event_id=event.id)



def export_timeline(
db_session,
timeline_filters: str,
incident_id: int,
):
incident = incident_service.get(db_session=db_session, incident_id=incident_id)
plugin = plugin_service.get_active_instance(
db_session=db_session, project_id=incident.project_id, plugin_type="document"
)
if not plugin:
log.error("Document not created. No storage plugin enabled.")
return False

"""gets timeline events for incident"""
event = get_by_incident_id(db_session=db_session, incident_id=incident_id)
table_data = []
dates = set()
data_inserted = False

"""Filters events based on user filter"""
for e in event.all():
time_header = "Time (UTC)"
event_timestamp = e.started_at.strftime("%Y-%m-%d %H:%M:%S")
if not e.owner:
e.owner = "Dispatch"
if timeline_filters.get("timezone").strip() == "America/Los_Angeles":
time_header = "Time (PST/PDT)"
event_timestamp = (
pytz.utc.localize(e.started_at)
.astimezone(pytz.timezone(timeline_filters.get("timezone").strip()))
.replace(tzinfo=None)
)
date, time = str(event_timestamp).split(" ")
if e.pinned or timeline_filters.get(e.type):
if date in dates:
table_data.append(
{time_header: time, "Description": e.description, "Owner": e.owner}
)
else:
dates.add(date)
table_data.append({time_header: date, "Description": "\t", "owner": "\t"})
table_data.append(
{time_header: time, "Description": e.description, "Owner": e.owner}
)

if table_data:
table_data = json.loads(json.dumps(table_data))
num_columns = len(table_data[0].keys() if table_data else [])
column_headers = table_data[0].keys()

documents_list = []
if timeline_filters.get("incidentDocument"):
documents = document_service.get_by_incident_id_and_resource_type(
db_session=db_session,
incident_id=incident_id,
project_id=incident.project.id,
resource_type="dispatch-incident-document",
)
if documents:
documents_list.append(documents.resource_id)

if timeline_filters.get("reviewDocument"):
documents = document_service.get_by_incident_id_and_resource_type(
db_session=db_session,
incident_id=incident_id,
project_id=incident.project.id,
resource_type="dispatch-incident-review-document",
)
if documents:
documents_list.append(documents.resource_id)

for doc_id in documents_list:
# Checks for existing table in the document
table_exists, curr_table_start, curr_table_end, _ = plugin.instance.get_table_details(
document_id=doc_id, header="Timeline"
)

# Deletes existing table
if table_exists:
delete_table_request = [
{
"deleteContentRange": {
"range": {
"segmentId": "",
"startIndex": curr_table_start,
"endIndex": curr_table_end,
}
}
}
]
if plugin.instance.delete_table(document_id=doc_id, request=delete_table_request):
log.debug("Existing table in the doc has been deleted")

else:
curr_table_start += 1
# Insert new table with required rows & columns
insert_table_request = [
{
"insertTable": {
"rows": len(table_data) + 1,
"columns": num_columns,
"location": {"index": curr_table_start - 1},
}
}
]
if plugin.instance.insert(document_id=doc_id, request=insert_table_request):
log.debug("Table skeleton inserted successfully")

else:
return False

# Formatting & inserting empty table
insert_data_request = [
{
"updateTableCellStyle": {
"tableCellStyle": {
"backgroundColor": {
"color": {"rgbColor": {"green": 0.4, "red": 0.4, "blue": 0.4}}
}
},
"fields": "backgroundColor",
"tableRange": {
"columnSpan": 3,
"rowSpan": 1,
"tableCellLocation": {
"columnIndex": 0,
"rowIndex": 0,
"tableStartLocation": {"index": curr_table_start},
},
},
}
}
]

if plugin.instance.insert(document_id=doc_id, request=insert_data_request):
log.debug("Table Formatted successfully")

else:
return False

# Calculating table cell indices
_, _, _, cell_indices = plugin.instance.get_table_details(
document_id=doc_id, header="Timeline"
)

data_to_insert = list(column_headers) + [
item for row in table_data for item in row.values()
]
str_len = 0
row_idx = 0
insert_data_request = []
for index, text in zip(cell_indices, data_to_insert, strict=True):
# Adjusting index based on string length
new_idx = index + str_len

insert_data_request.append(
{"insertText": {"location": {"index": new_idx}, "text": text}}
)

# Header field formatting
if text in column_headers:
insert_data_request.append(
{
"updateTextStyle": {
"range": {"startIndex": new_idx, "endIndex": new_idx + len(text)},
"textStyle": {
"bold": True,
"foregroundColor": {
"color": {"rgbColor": {"red": 1, "green": 1, "blue": 1}}
},
"fontSize": {"magnitude": 10, "unit": "PT"},
},
"fields": "bold,foregroundColor",
}
}
)

# Formating for date rows
if text == "\t":
insert_data_request.append(
{
"updateTableCellStyle": {
"tableCellStyle": {
"backgroundColor": {
"color": {
"rgbColor": {"green": 0.8, "red": 0.8, "blue": 0.8}
}
}
},
"fields": "backgroundColor",
"tableRange": {
"columnSpan": 3,
"rowSpan": 1,
"tableCellLocation": {
"tableStartLocation": {"index": curr_table_start},
"columnIndex": 0,
"rowIndex": row_idx // 3,
},
},
}
}
)

# Formating for time column
if row_idx % num_columns == 0:
insert_data_request.append(
{
"updateTextStyle": {
"range": {"startIndex": new_idx, "endIndex": new_idx + len(text)},
"textStyle": {
"bold": True,
},
"fields": "bold",
}
}
)

row_idx += 1
str_len += len(text) if text else 0

data_inserted = plugin.instance.insert(document_id=doc_id, request=insert_data_request)
if not data_inserted:
return False
else:
log.error("No timeline data to export")
return False
return True
9 changes: 0 additions & 9 deletions src/dispatch/incident/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,15 +612,6 @@ def status_flow_dispatcher(
elif previous_status == IncidentStatus.stable:
incident_closed_status_flow(incident=incident, db_session=db_session)

if previous_status != current_status:
event_service.log_incident_event(
db_session=db_session,
source="Dispatch Core App",
description=f"The incident status has been changed from {previous_status.lower()} to {current_status.lower()}", # noqa
incident_id=incident.id,
type=EventType.assessment_updated,
)


@background_task
def incident_update_flow(
Expand Down
9 changes: 7 additions & 2 deletions src/dispatch/incident/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ def create(*, db_session, incident_in: IncidentCreate) -> Incident:
"status": incident.status,
"visibility": incident.visibility,
},
individual_id=incident_in.reporter.individual.id,
incident_id=incident.id,
owner=reporter_name,
pinned=True,
Expand Down Expand Up @@ -279,15 +280,19 @@ def create(*, db_session, incident_in: IncidentCreate) -> Incident:
)

# add observer (if engage_next_oncall is enabled)
incident_role = resolve_role(db_session=db_session, role=ParticipantRoleType.incident_commander, incident=incident)
incident_role = resolve_role(
db_session=db_session, role=ParticipantRoleType.incident_commander, incident=incident
)
if incident_role and incident_role.engage_next_oncall:
oncall_plugin = plugin_service.get_active_instance(
db_session=db_session, project_id=incident.project.id, plugin_type="oncall"
)
if not oncall_plugin:
log.debug("Resolved observer role not available since oncall plugin is not active.")
else:
oncall_email = oncall_plugin.instance.get_next_oncall(service_id=incident_role.service.external_id)
oncall_email = oncall_plugin.instance.get_next_oncall(
service_id=incident_role.service.external_id
)
if oncall_email:
participant_flows.add_participant(
oncall_email,
Expand Down
23 changes: 23 additions & 0 deletions src/dispatch/incident/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,29 @@ def update_custom_event(
)


@router.post(
"/{incident_id}/exportTimeline",
summary="Exports timeline events.",
dependencies=[Depends(PermissionsDependency([IncidentCommanderOrScribePermission]))],
)
def export_timeline_event(
db_session: DbSession,
organization: OrganizationSlug,
incident_id: PrimaryKey,
current_incident: CurrentIncident,
timeline_filters: dict,
current_user: CurrentUser,
background_tasks: BackgroundTasks,
):
result = background_tasks.add_task(
event_flows.export_timeline,
timeline_filters=timeline_filters,
incident_id=incident_id,
organization_slug=organization,
)
return result


@router.delete(
"/{incident_id}/event/{event_uuid}",
summary="Deletes a custom event.",
Expand Down
Loading