From ff74b5566dc6c964b30146fd07b8594ada786eed Mon Sep 17 00:00:00 2001 From: Janani Neelamekam Date: Mon, 4 Dec 2023 17:47:16 -0800 Subject: [PATCH 01/12] Timeline Export changes --- src/dispatch/event/flows.py | 15 ++ src/dispatch/event/service.py | 251 +++++++++++++++++- src/dispatch/incident/views.py | 23 ++ .../plugins/dispatch_google/docs/plugin.py | 90 ++++++- .../src/incident/TimelineExportDialog.vue | 157 +++++++++++ .../dispatch/src/incident/TimelineTab.vue | 79 ++---- .../static/dispatch/src/incident/api.js | 4 + .../static/dispatch/src/incident/store.js | 13 +- 8 files changed, 571 insertions(+), 61 deletions(-) create mode 100644 src/dispatch/static/dispatch/src/incident/TimelineExportDialog.vue diff --git a/src/dispatch/event/flows.py b/src/dispatch/event/flows.py index 5da3ae803736..7c43181fe65a 100644 --- a/src/dispatch/event/flows.py +++ b/src/dispatch/event/flows.py @@ -53,3 +53,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 diff --git a/src/dispatch/event/service.py b/src/dispatch/event/service.py index 9c56ee3eaff6..fe292b7290b0 100644 --- a/src/dispatch/event/service.py +++ b/src/dispatch/event/service.py @@ -2,6 +2,10 @@ from uuid import uuid4 import datetime import logging +import json +import pytz + +from requests import HTTPError from dispatch.auth import service as auth_service from dispatch.case import service as case_service @@ -10,14 +14,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]: @@ -27,6 +37,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) @@ -182,3 +193,239 @@ def delete_incident_event( event = get_by_uuid(db_session=db_session, uuid=uuid) delete(db_session=db_session, event_id=event.id) + + +def delete_incident_event( + db_session, + uuid: str, +): + """Deletes an event.""" + event = get_by_uuid(db_session=db_session, uuid=uuid) + log.debug("Deleting incident event") + log.debug(event) + log.debug(event.id) + 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 + if 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 == True or timeline_filters.get(e.type) == True: + 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") + + # Insert new table with required rows & columns + inser_table_request = [ + { + "insertTable": { + "rows": len(table_data) + 1, + "columns": num_columns, + "location": {"index": curr_table_start}, + } + } + ] + if plugin.instance.insert(document_id=doc_id, request=inser_table_request): + log.debug("Table skeleton inserted successfully") + + else: + return False + + # Formatting & inserting empty table + 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 + 1}, + }, + }, + } + } + ] + + if plugin.instance.insert(document_id=doc_id, request=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 + for index, text in zip(cell_indices, data_to_insert): + # Adjusting index based on string length + new_idx = index + str_len + + request = [{"insertText": {"location": {"index": new_idx}, "text": text}}] + + # Header field formatting + if text in column_headers: + 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": + 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 + 1}, + "columnIndex": 0, + "rowIndex": row_idx // 3, + }, + }, + } + } + ) + + # Formating for time column + if row_idx % num_columns == 0: + request.append( + { + "updateTextStyle": { + "range": {"startIndex": new_idx, "endIndex": new_idx + len(text)}, + "textStyle": { + "bold": True, + }, + "fields": "bold", + } + } + ) + + row_idx += 1 + str_len += len(text) + + data_inserted = plugin.instance.insert(document_id=doc_id, request=request) + if not data_inserted: + return False + else: + log.error("No timeline data to export") + return False + return True diff --git a/src/dispatch/incident/views.py b/src/dispatch/incident/views.py index d20d3bcdbb4f..9924ecabb924 100644 --- a/src/dispatch/incident/views.py +++ b/src/dispatch/incident/views.py @@ -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.", diff --git a/src/dispatch/plugins/dispatch_google/docs/plugin.py b/src/dispatch/plugins/dispatch_google/docs/plugin.py index a8c7f02ce56f..4fa5dc9d8f63 100644 --- a/src/dispatch/plugins/dispatch_google/docs/plugin.py +++ b/src/dispatch/plugins/dispatch_google/docs/plugin.py @@ -6,6 +6,7 @@ .. moduleauthor:: Kevin Glisson """ import unicodedata +import logging from typing import Any, List from dispatch.decorators import apply, counter, timer @@ -13,6 +14,9 @@ from dispatch.plugins.dispatch_google import docs as google_docs_plugin from dispatch.plugins.dispatch_google.common import get_service from dispatch.plugins.dispatch_google.config import GoogleConfiguration +from googleapiclient.errors import HttpError + +log = logging.getLogger(__name__) def remove_control_characters(s): @@ -26,7 +30,6 @@ def replace_text(client: Any, document_id: str, replacements: List[str]): requests.append( {"replaceAllText": {"containsText": {"text": k, "matchCase": "true"}, "replaceText": v}} ) - body = {"requests": requests} return client.batchUpdate(documentId=document_id, body=body).execute() @@ -55,3 +58,88 @@ def update(self, document_id: str, **kwargs): kwargs = {"{{" + k + "}}": v for k, v in kwargs.items()} client = get_service(self.configuration, "docs", "v1", self.scopes).documents() return replace_text(client, document_id, kwargs) + + def insert(self, document_id: str, request): + client = get_service(self.configuration, "docs", "v1", self.scopes).documents() + body = {"requests": request} + try: + response = client.batchUpdate(documentId=document_id, body=body).execute() + if "replies" in response and response["replies"]: + return True + + except HttpError as error: + log.exception(error) + return False + except Exception as e: + log.exception(e) + return False + + def get_table_details(self, document_id: str, header: str): + client = get_service(self.configuration, "docs", "v1", self.scopes).documents() + try: + document_content = ( + client.get(documentId=document_id).execute().get("body").get("content") + ) + start_index = 0 + end_index = 0 + header_index = 0 + past_header = False + header_section = False + table_exists = False + table_indices = [] + for i, element in enumerate(document_content): + if "paragraph" in element and "elements" in element["paragraph"]: + for item in element["paragraph"]["elements"]: + if "textRun" in item: + if item["textRun"]["content"].strip() == header: + header_index = element["endIndex"] + header_section = True + + elif header_section: + if ( + header_section + and item["textRun"]["content"].strip() + and "headingId" + not in element["paragraph"].get("paragraphStyle", {}) + ): + header_index = item["endIndex"] + # checking if we are past header in question + if any( + "headingId" in style + for style in element["paragraph"]["paragraphStyle"] + for style in element["paragraph"].get("paragraphStyle", {}) + ): + past_header = True + header_section = False + break + # Checking for table under the header + elif header_section and "table" in element and not past_header: + table_exists = True + start_index = element["startIndex"] + end_index = element["endIndex"] + table = element["table"] + for row in table["tableRows"]: + for cell in row["tableCells"]: + table_indices.append(cell["content"][0]["startIndex"]) + return table_exists, start_index, end_index, table_indices + except HttpError as error: + log.exception(error) + return table_exists, header_index, -1, table_indices + except Exception as e: + log.exception(e) + return table_exists, header_index, -1, table_indices + + def delete_table(self, document_id: str, request): + try: + client = get_service(self.configuration, "docs", "v1", self.scopes).documents() + body = {"requests": request} + response = client.batchUpdate(documentId=document_id, body=body).execute() + if "replies" in response and response["replies"]: + return True + + except HttpError as error: + log.exception(error) + return False + except Exception as e: + log.exception(e) + return False diff --git a/src/dispatch/static/dispatch/src/incident/TimelineExportDialog.vue b/src/dispatch/static/dispatch/src/incident/TimelineExportDialog.vue new file mode 100644 index 000000000000..4a18b733e6a2 --- /dev/null +++ b/src/dispatch/static/dispatch/src/incident/TimelineExportDialog.vue @@ -0,0 +1,157 @@ + + + + \ No newline at end of file diff --git a/src/dispatch/static/dispatch/src/incident/TimelineTab.vue b/src/dispatch/static/dispatch/src/incident/TimelineTab.vue index e632518309a3..0bfca8659fef 100644 --- a/src/dispatch/static/dispatch/src/incident/TimelineTab.vue +++ b/src/dispatch/static/dispatch/src/incident/TimelineTab.vue @@ -2,15 +2,10 @@ - - Export - + + + @@ -22,28 +17,16 @@
- + mdi-plus-circle-outlineAdd event
- + @@ -122,13 +105,8 @@
- + mdi-plus-circle-outlineAdd event
@@ -151,10 +129,10 @@ import { sum } from "lodash" import { mapFields } from "vuex-map-fields" import { mapActions } from "vuex" -import Util from "@/util" import { snakeToCamel, formatToUTC, formatToTimeZones } from "@/filters" import TimelineFilterDialog from "@/incident/TimelineFilterDialog.vue" +import TimelineExportDialog from "./TimelineExportDialog.vue" import EditEventDialog from "@/incident/EditEventDialog.vue" import DeleteEventDialog from "@/incident/DeleteEventDialog.vue" @@ -183,12 +161,12 @@ export default { TimelineFilterDialog, EditEventDialog, DeleteEventDialog, + TimelineExportDialog, }, data() { return { showDetails: false, - exportLoading: false, } }, @@ -215,31 +193,8 @@ export default { "showDeleteEventDialog", "showNewPreEventDialog", "togglePin", - ]), - exportToCSV() { - this.exportLoading = true - const selected_items = [] - let items = this.sortedEvents - items.forEach((item) => { - if (this.showItem(item)) { - selected_items.push(item) - } - }) - Util.exportCSV( - selected_items.map((item) => ({ - "Time (in UTC)": item.started_at, - Description: item.description, - Owner: this.extractOwner(item), - })), - this.name + "-timeline-export.csv" - ) - this.exportLoading = false - }, - showItem(event) { - if (event.pinned) return true - return !this.timeline_filters[eventTypeToFilter[event.type]] - }, + ]), iconItem(event) { if (event.description == "Incident created") return "mdi-flare" return eventTypeToIcon[event.type] @@ -256,6 +211,12 @@ export default { }) ) }, + showItem(event) { + if (event.pinned) { + return true + } + return !this.timeline_filters[eventTypeToFilter[event.type]] + }, isEditable(event) { return event.type == "Custom event" || event.type == "Imported message" }, @@ -272,4 +233,8 @@ export default { } + + + + \ No newline at end of file +.error { + color: rgb(243, 89, 89); + margin-top: -30px; +} + From d6e3e686233350cbfee7c1e9c8b9dc3f4eb7842d Mon Sep 17 00:00:00 2001 From: David Whittaker Date: Tue, 12 Dec 2023 20:07:29 -0800 Subject: [PATCH 03/12] Working on dialog box --- .../static/dispatch/src/incident/TimelineExportDialog.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dispatch/static/dispatch/src/incident/TimelineExportDialog.vue b/src/dispatch/static/dispatch/src/incident/TimelineExportDialog.vue index b96d2de0eab0..0c030a1f5add 100644 --- a/src/dispatch/static/dispatch/src/incident/TimelineExportDialog.vue +++ b/src/dispatch/static/dispatch/src/incident/TimelineExportDialog.vue @@ -31,7 +31,7 @@ From 3866d71d3ae5c90e5a3a9d5d4ab7704b64428580 Mon Sep 17 00:00:00 2001 From: Janani Neelamekam Date: Wed, 13 Dec 2023 15:08:19 -0800 Subject: [PATCH 04/12] Timeline export to doc --- src/dispatch/event/service.py | 33 +++++++++++-------- .../plugins/dispatch_google/docs/plugin.py | 1 + 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/dispatch/event/service.py b/src/dispatch/event/service.py index 1907d3bca5b3..12c990fa4f6c 100644 --- a/src/dispatch/event/service.py +++ b/src/dispatch/event/service.py @@ -229,7 +229,7 @@ def export_timeline( """Filters events based on user filter""" for e in event.all(): time_header = "Time (UTC)" - event_timestamp = e.started_at + 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": @@ -294,30 +294,32 @@ def export_timeline( "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 - inser_table_request = [ + insert_table_request = [ { "insertTable": { "rows": len(table_data) + 1, "columns": num_columns, - "location": {"index": curr_table_start}, + "location": {"index": curr_table_start - 1}, } } ] - if plugin.instance.insert(document_id=doc_id, request=inser_table_request): + 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 - request = [ + insert_data_request = [ { "updateTableCellStyle": { "tableCellStyle": { @@ -332,14 +334,14 @@ def export_timeline( "tableCellLocation": { "columnIndex": 0, "rowIndex": 0, - "tableStartLocation": {"index": curr_table_start + 1}, + "tableStartLocation": {"index": curr_table_start}, }, }, } } ] - if plugin.instance.insert(document_id=doc_id, request=request): + if plugin.instance.insert(document_id=doc_id, request=insert_data_request): log.debug("Table Formatted successfully") else: @@ -355,15 +357,18 @@ def export_timeline( ] str_len = 0 row_idx = 0 + insert_data_request = [] for index, text in zip(cell_indices, data_to_insert): # Adjusting index based on string length new_idx = index + str_len - request = [{"insertText": {"location": {"index": new_idx}, "text": text}}] + insert_data_request.append( + {"insertText": {"location": {"index": new_idx}, "text": text}} + ) # Header field formatting if text in column_headers: - request.append( + insert_data_request.append( { "updateTextStyle": { "range": {"startIndex": new_idx, "endIndex": new_idx + len(text)}, @@ -381,7 +386,7 @@ def export_timeline( # Formating for date rows if text == "\t": - request.append( + insert_data_request.append( { "updateTableCellStyle": { "tableCellStyle": { @@ -396,7 +401,7 @@ def export_timeline( "columnSpan": 3, "rowSpan": 1, "tableCellLocation": { - "tableStartLocation": {"index": curr_table_start + 1}, + "tableStartLocation": {"index": curr_table_start}, "columnIndex": 0, "rowIndex": row_idx // 3, }, @@ -407,7 +412,7 @@ def export_timeline( # Formating for time column if row_idx % num_columns == 0: - request.append( + insert_data_request.append( { "updateTextStyle": { "range": {"startIndex": new_idx, "endIndex": new_idx + len(text)}, @@ -422,7 +427,7 @@ def export_timeline( row_idx += 1 str_len += len(text) if text else 0 - data_inserted = plugin.instance.insert(document_id=doc_id, request=request) + data_inserted = plugin.instance.insert(document_id=doc_id, request=insert_data_request) if not data_inserted: return False else: diff --git a/src/dispatch/plugins/dispatch_google/docs/plugin.py b/src/dispatch/plugins/dispatch_google/docs/plugin.py index 4fa5dc9d8f63..346f6ca6154e 100644 --- a/src/dispatch/plugins/dispatch_google/docs/plugin.py +++ b/src/dispatch/plugins/dispatch_google/docs/plugin.py @@ -128,6 +128,7 @@ def get_table_details(self, document_id: str, header: str): except Exception as e: log.exception(e) return table_exists, header_index, -1, table_indices + return table_exists, header_index, -1, table_indices def delete_table(self, document_id: str, request): try: From 443101656c1c0c7dee57b744f1ec6e48569ad180 Mon Sep 17 00:00:00 2001 From: David Whittaker Date: Mon, 18 Dec 2023 16:41:30 -0800 Subject: [PATCH 05/12] Cleaning up Slack message import and ensuring owner population --- src/dispatch/event/flows.py | 2 ++ src/dispatch/incident/flows.py | 9 --------- src/dispatch/incident/service.py | 2 ++ .../plugins/dispatch_slack/incident/interactive.py | 4 ++-- src/dispatch/report/flows.py | 2 ++ src/dispatch/task/service.py | 2 ++ 6 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/dispatch/event/flows.py b/src/dispatch/event/flows.py index 7c43181fe65a..692aec126afd 100644 --- a/src/dispatch/event/flows.py +++ b/src/dispatch/event/flows.py @@ -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__, ) diff --git a/src/dispatch/incident/flows.py b/src/dispatch/incident/flows.py index 8976eec03aec..5b21408b977d 100644 --- a/src/dispatch/incident/flows.py +++ b/src/dispatch/incident/flows.py @@ -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( diff --git a/src/dispatch/incident/service.py b/src/dispatch/incident/service.py index 0f6c09612c7c..9d02a2bedf83 100644 --- a/src/dispatch/incident/service.py +++ b/src/dispatch/incident/service.py @@ -208,6 +208,8 @@ def create(*, db_session, incident_in: IncidentCreate) -> Incident: "status": incident.status, "visibility": incident.visibility, }, + individual_id=incident_in.reporter.individual.id, + owner=incident_in.reporter.individual.name, incident_id=incident.id, owner=reporter_name, pinned=True, diff --git a/src/dispatch/plugins/dispatch_slack/incident/interactive.py b/src/dispatch/plugins/dispatch_slack/incident/interactive.py index 27250703b5f2..919bf953d213 100644 --- a/src/dispatch/plugins/dispatch_slack/incident/interactive.py +++ b/src/dispatch/plugins/dispatch_slack/incident/interactive.py @@ -688,7 +688,7 @@ def handle_timeline_added_event( event_service.log_incident_event( db_session=db_session, source=f"Slack message from {individual.name}", - description=f'"{message_text}," said {individual.name}', + description=message_text, incident_id=context["subject"].id, individual_id=individual.id, started_at=message_ts_utc, @@ -1072,7 +1072,7 @@ def handle_add_timeline_submission_event( db_session=db_session, source=f"Slack message from {participant.individual.name}", started_at=event_dt_utc, - description=f'"{event_description}," said {participant.individual.name}', + description=event_description, incident_id=context["subject"].id, individual_id=participant.individual.id, type=EventType.imported_message, diff --git a/src/dispatch/report/flows.py b/src/dispatch/report/flows.py index 8fca3c5bfc5a..5844012c3474 100644 --- a/src/dispatch/report/flows.py +++ b/src/dispatch/report/flows.py @@ -68,6 +68,7 @@ def create_tactical_report( details={"conditions": conditions, "actions": actions, "needs": needs}, incident_id=incident_id, individual_id=participant.individual.id, + owner=participant.individual.name, ) # we send the tactical report to the conversation @@ -147,6 +148,7 @@ def create_executive_report( details={"current_status": current_status, "overview": overview, "next_steps": next_steps}, incident_id=incident_id, individual_id=participant.individual.id, + owner=participant.individual.name, ) # we create a new document for the executive report diff --git a/src/dispatch/task/service.py b/src/dispatch/task/service.py index 68235cd3c65f..06d9caa72eaa 100644 --- a/src/dispatch/task/service.py +++ b/src/dispatch/task/service.py @@ -129,8 +129,10 @@ def create(*, db_session, task_in: TaskCreate) -> Task: db_session=db_session, source="Dispatch Core App", description=f"New incident task created by {creator.individual.name}", + individual_id=creator.individual.id, details={"weblink": task.weblink}, incident_id=incident.id, + owner=creator.individual.name, ) db_session.add(task) From bd2674536a80c99b2b5a63276dfb91e6187bf08a Mon Sep 17 00:00:00 2001 From: David Whittaker Date: Mon, 18 Dec 2023 17:29:33 -0800 Subject: [PATCH 06/12] Handle case when user adds a Slack event without a user --- .../dispatch_slack/incident/interactive.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/dispatch/plugins/dispatch_slack/incident/interactive.py b/src/dispatch/plugins/dispatch_slack/incident/interactive.py index 919bf953d213..f4df2dc72da7 100644 --- a/src/dispatch/plugins/dispatch_slack/incident/interactive.py +++ b/src/dispatch/plugins/dispatch_slack/incident/interactive.py @@ -679,10 +679,14 @@ def handle_timeline_added_event( incident = incident_service.get(db_session=db_session, incident_id=context["subject"].id) # we fetch the individual who sent the message - message_sender_email = get_user_email(client=client, user_id=message_sender_id) - individual = individual_service.get_by_email_and_project( - db_session=db_session, email=message_sender_email, project_id=incident.project.id - ) + # if user is not found, we default to "Unknown" + try: + message_sender_email = get_user_email(client=client, user_id=message_sender_id) + individual = individual_service.get_by_email_and_project( + db_session=db_session, email=message_sender_email, project_id=incident.project.id + ) + except Exception: + individual = None # we log the event event_service.log_incident_event( @@ -690,10 +694,10 @@ def handle_timeline_added_event( source=f"Slack message from {individual.name}", description=message_text, incident_id=context["subject"].id, - individual_id=individual.id, + individual_id=individual.id if individual else None, started_at=message_ts_utc, type=EventType.imported_message, - owner=individual.name, + owner=individual.name if individual else "Unknown", ) From 2907eef8ac65384293b69e0535f421b31a084d65 Mon Sep 17 00:00:00 2001 From: David Whittaker Date: Mon, 18 Dec 2023 17:30:00 -0800 Subject: [PATCH 07/12] Handle case when user adds a Slack event without a user --- src/dispatch/plugins/dispatch_slack/incident/interactive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dispatch/plugins/dispatch_slack/incident/interactive.py b/src/dispatch/plugins/dispatch_slack/incident/interactive.py index f4df2dc72da7..aec662a4c029 100644 --- a/src/dispatch/plugins/dispatch_slack/incident/interactive.py +++ b/src/dispatch/plugins/dispatch_slack/incident/interactive.py @@ -697,7 +697,7 @@ def handle_timeline_added_event( individual_id=individual.id if individual else None, started_at=message_ts_utc, type=EventType.imported_message, - owner=individual.name if individual else "Unknown", + owner=individual.name if individual else None, ) From 09b98c80401cc73820c8095121ff0886e956d1e7 Mon Sep 17 00:00:00 2001 From: Janani Neelamekam Date: Tue, 19 Dec 2023 15:51:08 -0800 Subject: [PATCH 08/12] Changes to support export on historic incidents --- src/dispatch/event/service.py | 4 +--- .../plugins/dispatch_google/docs/plugin.py | 20 +++++++++++++------ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/dispatch/event/service.py b/src/dispatch/event/service.py index 12c990fa4f6c..9d1a3cbca870 100644 --- a/src/dispatch/event/service.py +++ b/src/dispatch/event/service.py @@ -202,8 +202,6 @@ def delete_incident_event( """Deletes an event.""" event = get_by_uuid(db_session=db_session, uuid=uuid) log.debug("Deleting incident event") - log.debug(event) - log.debug(event.id) delete(db_session=db_session, event_id=event.id) @@ -298,7 +296,7 @@ def export_timeline( } ] if plugin.instance.delete_table(document_id=doc_id, request=delete_table_request): - log.debug("Existing Table in the doc has been deleted") + log.debug("Existing table in the doc has been deleted") else: curr_table_start += 1 diff --git a/src/dispatch/plugins/dispatch_google/docs/plugin.py b/src/dispatch/plugins/dispatch_google/docs/plugin.py index 346f6ca6154e..797fadc26077 100644 --- a/src/dispatch/plugins/dispatch_google/docs/plugin.py +++ b/src/dispatch/plugins/dispatch_google/docs/plugin.py @@ -87,6 +87,8 @@ def get_table_details(self, document_id: str, header: str): header_section = False table_exists = False table_indices = [] + log.debug(document_content) + headingId = "" for i, element in enumerate(document_content): if "paragraph" in element and "elements" in element["paragraph"]: for item in element["paragraph"]["elements"]: @@ -94,20 +96,26 @@ def get_table_details(self, document_id: str, header: str): if item["textRun"]["content"].strip() == header: header_index = element["endIndex"] header_section = True + headingId = element["paragraph"].get("paragraphStyle")["headingId"] elif header_section: + # Gets the end index of any text below the header if ( header_section and item["textRun"]["content"].strip() - and "headingId" - not in element["paragraph"].get("paragraphStyle", {}) + and element["paragraph"].get("paragraphStyle")["headingId"] + != headingId ): header_index = item["endIndex"] # checking if we are past header in question - if any( - "headingId" in style - for style in element["paragraph"]["paragraphStyle"] - for style in element["paragraph"].get("paragraphStyle", {}) + if ( + any( + "headingId" in style + for style in element["paragraph"]["paragraphStyle"] + for style in element["paragraph"].get("paragraphStyle", {}) + ) + and element["paragraph"].get("paragraphStyle")["headingId"] + != headingId ): past_header = True header_section = False From e1854a9c8527aa93d73c4cff241d74dd717c00e7 Mon Sep 17 00:00:00 2001 From: Janani Neelamekam Date: Tue, 19 Dec 2023 16:56:58 -0800 Subject: [PATCH 09/12] Formatting changes --- src/dispatch/incident/service.py | 9 +++-- .../plugins/dispatch_google/docs/plugin.py | 8 +---- .../src/case/CaseStatusSelectGroup.vue | 18 +++++----- .../src/incident/TimelineExportDialog.vue | 28 +++++++-------- .../dispatch/src/incident/TimelineTab.vue | 34 +++++++++++++------ .../static/dispatch/src/incident/api.js | 2 +- .../static/dispatch/src/incident/store.js | 2 +- 7 files changed, 55 insertions(+), 46 deletions(-) diff --git a/src/dispatch/incident/service.py b/src/dispatch/incident/service.py index 9d02a2bedf83..1048dc0b5e3d 100644 --- a/src/dispatch/incident/service.py +++ b/src/dispatch/incident/service.py @@ -209,7 +209,6 @@ def create(*, db_session, incident_in: IncidentCreate) -> Incident: "visibility": incident.visibility, }, individual_id=incident_in.reporter.individual.id, - owner=incident_in.reporter.individual.name, incident_id=incident.id, owner=reporter_name, pinned=True, @@ -281,7 +280,9 @@ 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" @@ -289,7 +290,9 @@ def create(*, db_session, incident_in: IncidentCreate) -> Incident: 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, diff --git a/src/dispatch/plugins/dispatch_google/docs/plugin.py b/src/dispatch/plugins/dispatch_google/docs/plugin.py index 797fadc26077..1d52a873e71e 100644 --- a/src/dispatch/plugins/dispatch_google/docs/plugin.py +++ b/src/dispatch/plugins/dispatch_google/docs/plugin.py @@ -87,7 +87,6 @@ def get_table_details(self, document_id: str, header: str): header_section = False table_exists = False table_indices = [] - log.debug(document_content) headingId = "" for i, element in enumerate(document_content): if "paragraph" in element and "elements" in element["paragraph"]: @@ -100,12 +99,7 @@ def get_table_details(self, document_id: str, header: str): elif header_section: # Gets the end index of any text below the header - if ( - header_section - and item["textRun"]["content"].strip() - and element["paragraph"].get("paragraphStyle")["headingId"] - != headingId - ): + if header_section and item["textRun"]["content"].strip(): header_index = item["endIndex"] # checking if we are past header in question if ( diff --git a/src/dispatch/static/dispatch/src/case/CaseStatusSelectGroup.vue b/src/dispatch/static/dispatch/src/case/CaseStatusSelectGroup.vue index 5859c3643bf6..f1b67632975c 100644 --- a/src/dispatch/static/dispatch/src/case/CaseStatusSelectGroup.vue +++ b/src/dispatch/static/dispatch/src/case/CaseStatusSelectGroup.vue @@ -5,13 +5,13 @@ Update Case Status Are you sure you want to change the case status from - {{ - modelValue.status - }} + + {{ modelValue.status }} + to - {{ - selectedStatus - }} + + {{ selectedStatus }} + ? @@ -27,9 +27,9 @@ Status Not Changed This case was moved to the status - {{ - selectedStatus - }} + + {{ selectedStatus }} + on