From d6c728a58125fc94b234659b890f27968eb1635f Mon Sep 17 00:00:00 2001 From: Marc Vilanova Date: Thu, 11 Aug 2022 14:09:15 -0700 Subject: [PATCH] Adds support for deleting external tickets, groups, and storage --- src/dispatch/case/flows.py | 30 ++++++++++---- src/dispatch/case/views.py | 41 ++++++++++++++----- src/dispatch/group/flows.py | 15 +++---- .../plugins/dispatch_google/drive/drive.py | 4 +- .../plugins/dispatch_google/drive/plugin.py | 27 ++++++------ src/dispatch/plugins/dispatch_jira/plugin.py | 6 +++ src/dispatch/static/dispatch/src/api.js | 7 +++- src/dispatch/storage/flows.py | 15 +++---- src/dispatch/ticket/flows.py | 15 +++---- 9 files changed, 98 insertions(+), 62 deletions(-) diff --git a/src/dispatch/case/flows.py b/src/dispatch/case/flows.py index c910008fb6e0..bceacd6c5e8f 100644 --- a/src/dispatch/case/flows.py +++ b/src/dispatch/case/flows.py @@ -43,7 +43,7 @@ def case_new_create_flow(*, case_id: int, organization_slug: str, db_session=Non if not group: # we delete the ticket - ticket_flows.delete_ticket(ticket=ticket, project_id=case.project.id, db_session=db_session) + ticket_flows.delete_ticket(ticket=ticket, db_session=db_session) # we delete the case delete(db_session=db_session, case_id=case_id) @@ -53,10 +53,10 @@ def case_new_create_flow(*, case_id: int, organization_slug: str, db_session=Non storage = storage_flows.create_storage(obj=case, members=members, db_session=db_session) if not storage: # we delete the group - group_flows.delete_group(group=group, project_id=case.project.id, db_session=db_session) + group_flows.delete_group(group=group, db_session=db_session) # we delete the ticket - ticket_flows.delete_ticket(ticket=ticket, project_id=case.project.id, db_session=db_session) + ticket_flows.delete_ticket(ticket=ticket, db_session=db_session) # we delete the case delete(db_session=db_session, case_id=case_id) @@ -70,15 +70,13 @@ def case_new_create_flow(*, case_id: int, organization_slug: str, db_session=Non ) if not document: # we delete the storage - storage_flows.delete_storage( - storage=storage, project_id=case.project.id, db_session=db_session - ) + storage_flows.delete_storage(storage=storage, db_session=db_session) # we delete the group - group_flows.delete_group(group=group, project_id=case.project.id, db_session=db_session) + group_flows.delete_group(group=group, db_session=db_session) # we delete the ticket - ticket_flows.delete_ticket(ticket=ticket, project_id=case.project.id, db_session=db_session) + ticket_flows.delete_ticket(ticket=ticket, db_session=db_session) # we delete the case delete(db_session=db_session, case_id=case_id) @@ -139,6 +137,22 @@ def case_update_flow( # we send the case updated notification +def case_delete_flow(case: Case, db_session: SessionLocal): + """Runs the case delete flow.""" + # we delete the external ticket + if case.ticket: + ticket_flows.delete_ticket(ticket=case.ticket, db_session=db_session) + + # we delete the external groups + if case.groups: + for group in case.groups: + group_flows.delete_group(group=group, db_session=db_session) + + # we delete the external storage + if case.storage: + storage_flows.delete_storage(storage=case.storage, db_session=db_session) + + def case_new_status_flow(case: Case, db_session=None): """Runs the case new transition flow.""" pass diff --git a/src/dispatch/case/views.py b/src/dispatch/case/views.py index 50cc349778cc..ba5d3f21de70 100644 --- a/src/dispatch/case/views.py +++ b/src/dispatch/case/views.py @@ -7,6 +7,7 @@ from starlette.requests import Request from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, status +from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session # NOTE: define permissions before enabling the code block below @@ -26,6 +27,7 @@ from .flows import ( case_closed_create_flow, + case_delete_flow, case_escalated_create_flow, case_new_create_flow, case_triage_create_flow, @@ -41,7 +43,7 @@ def get_current_case(*, db_session: Session = Depends(get_db), request: Request) -> Case: - """Fetches case or returns an HTTP 404.""" + """Fetches a case or returns an HTTP 404.""" case = get(db_session=db_session, case_id=request.path_params["case_id"]) if not case: raise HTTPException( @@ -51,13 +53,13 @@ def get_current_case(*, db_session: Session = Depends(get_db), request: Request) return case -@router.get("", summary="Retrieve a list of all cases.") +@router.get("", summary="Retrieves all cases.") def get_cases( *, common: dict = Depends(common_parameters), include: List[str] = Query([], alias="include[]"), ): - """Retrieve a list of all cases.""" + """Retrieves all cases.""" pagination = search_filter_sort_paginate(model="Case", **common) if include: @@ -74,7 +76,7 @@ def get_cases( return json.loads(CasePagination(**pagination).json()) -@router.post("", response_model=CaseRead, summary="Create a new case.") +@router.post("", response_model=CaseRead, summary="Creates a new case.") def create_case( *, db_session: Session = Depends(get_db), @@ -129,7 +131,7 @@ def update_case( current_user: DispatchUser = Depends(get_current_user), background_tasks: BackgroundTasks, ): - """Update an existing case.""" + """Updates an existing case.""" # we store the previous state of the case in order to be able to detect changes previous_case = CaseRead.from_orm(current_case) @@ -151,14 +153,33 @@ def update_case( @router.delete( "/{case_id}", response_model=CaseRead, - summary="Delete an case.", + summary="Deletes an existing case.", # dependencies=[Depends(PermissionsDependency([CaseEditPermission]))], ) def delete_case( *, - case_id: PrimaryKey, db_session: Session = Depends(get_db), - current_case: Case = Depends(get_current_case), + organization: OrganizationSlug, + case_id: PrimaryKey, + background_tasks: BackgroundTasks, ): - """Delete an individual case.""" - delete(db_session=db_session, case_id=current_case.id) + """Deletes an existing case.""" + # we get the internal case + case = get(db_session=db_session, case_id=case_id) + + # we run the case delete flow + case_delete_flow(case=case, db_session=db_session) + + # we delete the internal case + try: + delete(db_session=db_session, case_id=case_id) + except IntegrityError as e: + log.exception(e) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=[ + { + "msg": f"Case {case.name} could not be deleted. Make sure the case has no relationships to other cases or incidents before deleting it." + } + ], + ) diff --git a/src/dispatch/group/flows.py b/src/dispatch/group/flows.py index 1b53c6676f45..338e6d3d7195 100644 --- a/src/dispatch/group/flows.py +++ b/src/dispatch/group/flows.py @@ -81,18 +81,15 @@ def create_group( return group -def delete_group(group: Group, project_id: int, db_session: SessionLocal): +def delete_group(group: Group, db_session: SessionLocal): """Deletes an existing group.""" - # we delete the external group plugin = plugin_service.get_active_instance( - db_session=db_session, project_id=project_id, plugin_type="participant-group" + db_session=db_session, project_id=group.case.project.id, plugin_type="participant-group" ) if plugin: - # TODO(mvilanova): implement deleting the external group - # plugin.instance.delete() - pass + try: + plugin.instance.delete(email=group.email) + except Exception as e: + log.exception(e) else: log.warning("Group not deleted. No group plugin enabled.") - - # we delete the internal group - delete(db_session=db_session, group_id=group.id) diff --git a/src/dispatch/plugins/dispatch_google/drive/drive.py b/src/dispatch/plugins/dispatch_google/drive/drive.py index 310ffed7b5cf..8ea11da64b35 100644 --- a/src/dispatch/plugins/dispatch_google/drive/drive.py +++ b/src/dispatch/plugins/dispatch_google/drive/drive.py @@ -255,8 +255,8 @@ def copy_file(client: Any, folder_id: str, file_id: str, new_file_name: str): ) -def delete_file(client: Any, folder_id: str, file_id: str): - """Deletes a file from a teamdrive.""" +def delete_file(client: Any, file_id: str): + """Deletes a folder or file from a Google Drive.""" return make_call(client.files(), "delete", fileId=file_id, supportsAllDrives=True) diff --git a/src/dispatch/plugins/dispatch_google/drive/plugin.py b/src/dispatch/plugins/dispatch_google/drive/plugin.py index 539e491783ac..187bcd912450 100644 --- a/src/dispatch/plugins/dispatch_google/drive/plugin.py +++ b/src/dispatch/plugins/dispatch_google/drive/plugin.py @@ -5,21 +5,21 @@ from dispatch.plugins.bases import StoragePlugin, TaskPlugin from dispatch.plugins.dispatch_google import drive as google_drive_plugin from dispatch.plugins.dispatch_google.common import get_service - from dispatch.plugins.dispatch_google.config import GoogleConfiguration + from .drive import ( Roles, + add_domain_permission, add_permission, + add_reply, copy_file, create_file, delete_file, download_google_document, list_files, + mark_as_readonly, move_file, remove_permission, - add_domain_permission, - add_reply, - mark_as_readonly, ) from .task import get_task_activity @@ -72,13 +72,13 @@ def add_participant( role: str = "owner", user_type: str = "user", ): - """Adds participants to existing Google Drive.""" + """Adds participants to an existing Google Drive.""" client = get_service(self.configuration, "drive", "v3", self.scopes) for p in participants: add_permission(client, p, team_drive_or_file_id, role, user_type) def remove_participant(self, folder_id: str, participants: List[str]): - """Removes participants from existing Google Drive.""" + """Removes participants from an existing Google Drive.""" client = get_service(self.configuration, "drive", "v3", self.scopes) for p in participants: remove_permission(client, p, folder_id) @@ -101,35 +101,34 @@ def create_file( role: str = Roles.writer, file_type: str = "folder", ): - """Creates a new file in existing Google Drive.""" + """Creates a new file in an existing Google Drive.""" client = get_service(self.configuration, "drive", "v3", self.scopes) response = create_file(client, parent_id, name, participants, role, file_type) response["weblink"] = response["webViewLink"] return response - def delete_file(self, folder_id: str, file_id: str): - """Removes a file from existing Google Drive.""" + def delete_file(self, file_id: str): + """Deletes a file or folder from an existing Google Drive.""" client = get_service(self.configuration, "drive", "v3", self.scopes) - response = delete_file(client, folder_id, file_id) - response["weblink"] = response["webViewLink"] + response = delete_file(client, file_id) return response def copy_file(self, folder_id: str, file_id: str, name: str): - """Creates a copy of the given file and places it in the specified team drive.""" + """Creates a copy of the given file and places it in the specified Google Drive.""" client = get_service(self.configuration, "drive", "v3", self.scopes) response = copy_file(client, folder_id, file_id, name) response["weblink"] = response["webViewLink"] return response def move_file(self, new_folder_id: str, file_id: str): - """Moves a file from one team drive to another.""" + """Moves a file from one Google drive to another.""" client = get_service(self.configuration, "drive", "v3", self.scopes) response = move_file(client, new_folder_id, file_id) response["weblink"] = response["webViewLink"] return response def list_files(self, folder_id: str, q: str = None): - """Lists all files in team drive.""" + """Lists all files in a Google drive.""" client = get_service(self.configuration, "drive", "v3", self.scopes) return list_files(client, folder_id, q) diff --git a/src/dispatch/plugins/dispatch_jira/plugin.py b/src/dispatch/plugins/dispatch_jira/plugin.py index 31d31f2167cb..27e02e8374f8 100644 --- a/src/dispatch/plugins/dispatch_jira/plugin.py +++ b/src/dispatch/plugins/dispatch_jira/plugin.py @@ -393,3 +393,9 @@ def update_case_ticket( ) return update(self.configuration, client, issue, issue_fields, status) + + def delete(self, ticket_id: str): + """Deletes a Jira issue.""" + client = create_client(self.configuration) + issue = client.issue(ticket_id) + issue.delete() diff --git a/src/dispatch/static/dispatch/src/api.js b/src/dispatch/static/dispatch/src/api.js index be687d18d964..6ce864619d4a 100644 --- a/src/dispatch/static/dispatch/src/api.js +++ b/src/dispatch/static/dispatch/src/api.js @@ -100,10 +100,15 @@ instance.interceptors.response.use( } if (err.response.status == 500) { + let errorText = err.response.data.detail.map(({ msg }) => msg).join(" ") + if (errorText.length == 0) { + errorText = + "Something has gone wrong, please retry or let your admin know that you received this error." + } store.commit( "notification_backend/addBeNotification", { - text: "Something has gone wrong, please retry or let your admin know that you received this error.", + text: errorText, type: "error", }, { root: true } diff --git a/src/dispatch/storage/flows.py b/src/dispatch/storage/flows.py index 8b22bf710667..e48778c003cd 100644 --- a/src/dispatch/storage/flows.py +++ b/src/dispatch/storage/flows.py @@ -75,18 +75,15 @@ def create_storage(obj: Any, members: List[str], db_session: SessionLocal): return storage -def delete_storage(storage: Storage, project_id: int, db_session: SessionLocal): +def delete_storage(storage: Storage, db_session: SessionLocal): """Deletes an existing storage.""" - # we delete the external storage plugin = plugin_service.get_active_instance( - db_session=db_session, project_id=project_id, plugin_type="storage" + db_session=db_session, project_id=storage.case.project.id, plugin_type="storage" ) if plugin: - # TODO(mvilanova): implement deleting the external storage - # plugin.instance.delete() - pass + try: + plugin.instance.delete_file(file_id=storage.resource_id) + except Exception as e: + log.exception(e) else: log.warning("Storage not deleted. No storage plugin enabled.") - - # we delete the internal storage - delete(db_session=db_session, storage_id=storage.id) diff --git a/src/dispatch/ticket/flows.py b/src/dispatch/ticket/flows.py index 48cd71c24991..4257df89c15f 100644 --- a/src/dispatch/ticket/flows.py +++ b/src/dispatch/ticket/flows.py @@ -213,18 +213,15 @@ def update_case_ticket( ) -def delete_ticket(ticket: Ticket, project_id: int, db_session: SessionLocal): +def delete_ticket(ticket: Ticket, db_session: SessionLocal): """Deletes a ticket.""" - # we delete the external ticket plugin = plugin_service.get_active_instance( - db_session=db_session, project_id=project_id, plugin_type="ticket" + db_session=db_session, project_id=ticket.case.project.id, plugin_type="ticket" ) if plugin: - # TODO(mvilanova): implement deleting the external ticket - # plugin.instance.delete_case_ticket() - pass + try: + plugin.instance.delete(ticket_id=ticket.resource_id) + except Exception as e: + log.exception(e) else: log.warning("Ticket not deleted. No ticket plugin enabled.") - - # we delete the internal ticket - delete(db_session=db_session, ticket_id=ticket.id)