Skip to content

feat(case): adds ability to create and edit custom timeline events for cases #6067

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
14 changes: 14 additions & 0 deletions src/dispatch/auth/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,20 @@ def has_required_permissions(
return True


class CaseEventPermission(BasePermission):
def has_required_permissions(
self,
request: Request,
) -> bool:
return any_permission(
permissions=[
OrganizationAdminPermission,
CaseParticipantPermission,
],
request=request,
)


class FeedbackDeletePermission(BasePermission):
def has_required_permissions(
self,
Expand Down
1 change: 1 addition & 0 deletions src/dispatch/case/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ def create(*, db_session, case_in: CaseCreate, current_user: DispatchUser = None
"visibility": case.visibility,
},
case_id=case.id,
pinned=True,
)

assignee_email = None
Expand Down
125 changes: 124 additions & 1 deletion src/dispatch/case/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import logging
from datetime import datetime
from typing import Annotated

from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, status
Expand All @@ -11,13 +12,16 @@
CaseEditPermission,
CaseJoinPermission,
CaseViewPermission,
CaseEventPermission,
PermissionsDependency,
)
from dispatch.auth.service import CurrentUser
from dispatch.case.enums import CaseStatus
from dispatch.common.utils.views import create_pydantic_include
from dispatch.database.core import DbSession
from dispatch.database.service import CommonParameters, search_filter_sort_paginate
from dispatch.event import flows as event_flows
from dispatch.event.models import EventCreateMinimal, EventUpdate
from dispatch.incident import service as incident_service
from dispatch.incident.models import IncidentCreate, IncidentRead
from dispatch.individual.models import IndividualContactRead
Expand All @@ -39,7 +43,15 @@
case_update_flow,
get_case_participants_flow,
)
from .models import Case, CaseCreate, CaseExpandedPagination, CasePagination, CasePaginationMinimalWithExtras, CaseRead, CaseUpdate
from .models import (
Case,
CaseCreate,
CaseExpandedPagination,
CasePagination,
CasePaginationMinimalWithExtras,
CaseRead,
CaseUpdate,
)
from .service import create, delete, get, get_participants, update

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -144,6 +156,7 @@ def get_cases_minimal(

return json.loads(CasePaginationMinimalWithExtras(**pagination).json())


@router.post("", response_model=CaseRead, summary="Creates a new case.")
def create_case(
db_session: DbSession,
Expand Down Expand Up @@ -413,3 +426,113 @@ def join_case(
case_id=current_case.id,
organization_slug=organization,
)


@router.post(
"/{case_id}/event",
summary="Creates a custom event.",
dependencies=[Depends(PermissionsDependency([CaseEventPermission]))],
)
def create_custom_event(
db_session: DbSession,
organization: OrganizationSlug,
case_id: PrimaryKey,
current_case: CurrentCase,
event_in: EventCreateMinimal,
current_user: CurrentUser,
background_tasks: BackgroundTasks,
):
if event_in.details is None:
event_in.details = {}
event_in.details.update({"created_by": current_user.email, "added_on": str(datetime.utcnow())})
"""Creates a custom event."""
background_tasks.add_task(
event_flows.log_case_event,
user_email=current_user.email,
case_id=current_case.id,
event_in=event_in,
organization_slug=organization,
)


@router.patch(
"/{case_id}/event",
summary="Updates a custom event.",
dependencies=[Depends(PermissionsDependency([CaseEventPermission]))],
)
def update_custom_event(
db_session: DbSession,
organization: OrganizationSlug,
case_id: PrimaryKey,
current_case: CurrentCase,
event_in: EventUpdate,
current_user: CurrentUser,
background_tasks: BackgroundTasks,
):
if event_in.details:
event_in.details.update(
{
**event_in.details,
"updated_by": current_user.email,
"updated_on": str(datetime.utcnow()),
}
)
else:
event_in.details = {"updated_by": current_user.email, "updated_on": str(datetime.utcnow())}
"""Updates a custom event."""
background_tasks.add_task(
event_flows.update_case_event,
event_in=event_in,
organization_slug=organization,
)


@router.post(
"/{case_id}/exportTimeline",
summary="Exports timeline events.",
dependencies=[Depends(PermissionsDependency([CaseEventPermission]))],
)
def export_timeline_event(
db_session: DbSession,
organization: OrganizationSlug,
case_id: PrimaryKey,
current_case: CurrentCase,
timeline_filters: dict,
current_user: CurrentUser,
background_tasks: BackgroundTasks,
):
try:
event_flows.export_case_timeline(
timeline_filters=timeline_filters,
case_id=case_id,
organization_slug=organization,
db_session=db_session,
)
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=[{"msg": (f"{str(e)}.",)}],
) from e


@router.delete(
"/{case_id}/event/{event_uuid}",
summary="Deletes a custom event.",
dependencies=[Depends(PermissionsDependency([CaseEventPermission]))],
)
def delete_custom_event(
db_session: DbSession,
organization: OrganizationSlug,
case_id: PrimaryKey,
current_case: CurrentCase,
event_uuid: str,
current_user: CurrentUser,
background_tasks: BackgroundTasks,
):
"""Deletes a custom event."""
background_tasks.add_task(
event_flows.delete_case_event,
event_uuid=event_uuid,
organization_slug=organization,
)
41 changes: 29 additions & 12 deletions src/dispatch/document/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ def get_by_incident_id_and_resource_type(
)


def get_by_case_id_and_resource_type(
*, db_session, case_id: int, project_id: int, resource_type: str
) -> Document | None:
"""Returns a document based on the given case and id and document resource type."""
return (
db_session.query(Document)
.filter(Document.case_id == case_id)
.filter(Document.project_id == project_id)
.filter(Document.resource_type == resource_type)
.one_or_none()
)


def get_project_forms_export_template(*, db_session, project_id: int) -> Document | None:
"""Fetches the project forms export template."""
resource_type = DocumentResourceTemplateTypes.forms
Expand Down Expand Up @@ -89,12 +102,14 @@ def create(*, db_session, document_in: DocumentCreate) -> Document:
.one_or_none()
)
if faq_doc:
raise ValidationError([
{
"msg": "FAQ document already defined for this project.",
"loc": "document",
}
])
raise ValidationError(
[
{
"msg": "FAQ document already defined for this project.",
"loc": "document",
}
]
)

if document_in.resource_type == DocumentResourceTemplateTypes.forms:
forms_doc = (
Expand All @@ -104,12 +119,14 @@ def create(*, db_session, document_in: DocumentCreate) -> Document:
.one_or_none()
)
if forms_doc:
raise ValidationError([
{
"msg": "Forms export template document already defined for this project.",
"loc": "document",
}
])
raise ValidationError(
[
{
"msg": "Forms export template document already defined for this project.",
"loc": "document",
}
]
)

filters = [
search_filter_service.get(db_session=db_session, search_filter_id=f.id)
Expand Down
70 changes: 70 additions & 0 deletions src/dispatch/event/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
from dispatch.decorators import background_task
from dispatch.event import service as event_service
from dispatch.incident import service as incident_service
from dispatch.case import service as case_service
from dispatch.individual import service as individual_service
from dispatch.event.models import EventUpdate, EventCreateMinimal
from dispatch.auth import service as auth_service

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -71,3 +73,71 @@ def export_timeline(

except Exception:
raise


@background_task
def log_case_event(
user_email: str,
case_id: int,
event_in: EventCreateMinimal,
db_session=None,
organization_slug: str = None,
):
case = case_service.get(db_session=db_session, case_id=case_id)
individual = individual_service.get_by_email_and_project(
db_session=db_session, email=user_email, project_id=case.project.id
)
event_in.source = f"Custom event created by {individual.name}"
event_in.owner = individual.name

# Get dispatch user by email
dispatch_user = auth_service.get_by_email(db_session=db_session, email=user_email)
dispatch_user_id = dispatch_user.id if dispatch_user else None

event_service.log_case_event(
db_session=db_session,
case_id=case_id,
dispatch_user_id=dispatch_user_id,
**event_in.__dict__,
)


@background_task
def update_case_event(
event_in: EventUpdate,
db_session=None,
organization_slug: str = None,
):
event_service.update_case_event(
db_session=db_session,
event_in=event_in,
)


@background_task
def delete_case_event(
event_uuid: str,
db_session=None,
organization_slug: str = None,
):
event_service.delete_case_event(
db_session=db_session,
uuid=event_uuid,
)


def export_case_timeline(
timeline_filters: dict,
case_id: int,
db_session=None,
organization_slug: str = None,
):
try:
event_service.export_case_timeline(
db_session=db_session,
timeline_filters=timeline_filters,
case_id=case_id,
)

except Exception:
raise
2 changes: 1 addition & 1 deletion src/dispatch/event/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class EventCreateMinimal(DispatchBase):
started_at: datetime
source: str
description: str
details: dict
details: dict | None = None
type: str | None = None
owner: str | None = None
pinned: bool | None = False
Loading
Loading