diff --git a/docs/package-lock.json b/docs/package-lock.json index ca1634adf87f..ca6657e47ef9 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -9227,9 +9227,9 @@ } }, "node_modules/postcss": { - "version": "8.4.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", - "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "funding": [ { "type": "opencollective", @@ -9238,10 +9238,14 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, diff --git a/requirements-base.txt b/requirements-base.txt index 8adb6256666f..d088dd9b5dc8 100644 --- a/requirements-base.txt +++ b/requirements-base.txt @@ -77,10 +77,14 @@ click==8.1.7 # schemathesis # typer # uvicorn +cloudpathlib==0.15.1 + # via weasel colorama==0.4.6 # via schemathesis confection==0.1.1 - # via thinc + # via + # thinc + # weasel cryptography==39.0.2 # via # -r requirements-base.in @@ -128,7 +132,7 @@ frozenlist==1.4.0 # aiosignal google-api-core==2.11.1 # via google-api-python-client -google-api-python-client==2.101.0 +google-api-python-client==2.102.0 # via -r requirements-base.in google-auth==2.22.0 # via @@ -255,6 +259,7 @@ packaging==23.1 # spacy # statsmodels # thinc + # weasel pandas==2.1.1 # via # -r requirements-base.in @@ -277,12 +282,12 @@ preshed==3.0.8 # via # spacy # thinc -protobuf==4.24.3 +protobuf==4.24.4 # via # -r requirements-base.in # google-api-core # googleapis-common-protos -psycopg2-binary==2.9.8 +psycopg2-binary==2.9.9 # via -r requirements-base.in pyasn1==0.5.0 # via @@ -304,6 +309,7 @@ pydantic==1.10.13 # fastapi # spacy # thinc + # weasel pyjwt[crypto]==2.8.0 # via # msal @@ -357,6 +363,7 @@ requests==2.31.0 # schemathesis # spacy # starlette-testclient + # weasel requests-oauthlib==1.3.1 # via # atlassian-python-api @@ -411,6 +418,7 @@ smart-open==6.3.0 # via # pathy # spacy + # weasel sniffio==1.3.0 # via # anyio @@ -418,7 +426,7 @@ sniffio==1.3.0 # httpx sortedcontainers==2.4.0 # via hypothesis -spacy==3.6.1 +spacy==3.7.1 # via -r requirements-base.in spacy-legacy==3.0.12 # via spacy @@ -439,6 +447,7 @@ srsly==2.4.7 # confection # spacy # thinc + # weasel starlette==0.27.0 # via # fastapi @@ -468,6 +477,7 @@ typer==0.9.0 # via # pathy # spacy + # weasel typing-extensions==4.7.1 # via # alembic @@ -480,7 +490,7 @@ tzdata==2023.3 # via pandas uritemplate==4.1.1 # via google-api-python-client -urllib3==1.26.16 +urllib3==1.26.17 # via # botocore # google-auth @@ -497,6 +507,9 @@ wasabi==1.1.2 # via # spacy # thinc + # weasel +weasel==0.3.2 + # via spacy werkzeug==2.3.7 # via schemathesis wrapt==1.15.0 diff --git a/src/dispatch/auth/permissions.py b/src/dispatch/auth/permissions.py index 10cd4b3beb94..d4936f03b3e4 100644 --- a/src/dispatch/auth/permissions.py +++ b/src/dispatch/auth/permissions.py @@ -15,6 +15,7 @@ from dispatch.models import PrimaryKeyModel from dispatch.organization import service as organization_service from dispatch.organization.models import OrganizationRead +from dispatch.participant_role.enums import ParticipantRoleType log = logging.getLogger(__name__) @@ -315,6 +316,8 @@ def has_required_permissions( if current_incident.reporter.individual.email == current_user.email: return True + return False + class IncidentCommanderPermission(BasePermission): def has_required_permissions( @@ -331,6 +334,36 @@ def has_required_permissions( if current_incident.commander.individual.email == current_user.email: return True + return False + + +class IncidentCommanderOrScribePermission(BasePermission): + def has_required_permissions( + self, + request: Request, + ) -> bool: + current_user = get_current_user(request=request) + pk = PrimaryKeyModel(id=request.path_params["incident_id"]) + current_incident = incident_service.get(db_session=request.state.db, incident_id=pk.id) + if not current_incident: + return False + + if ( + current_incident.commander + and current_incident.commander.individual.email == current_user.email + ): + return True + + scribes = [ + participant.individual.email + for participant in current_incident.participants + if ParticipantRoleType.scribe in [role.role for role in participant.participant_roles] + ] + if current_user.email in scribes: + return True + + return False + class IncidentParticipantPermission(BasePermission): def has_required_permissions( diff --git a/src/dispatch/database/revisions/tenant/versions/2023-09-15_3538650dc471.py b/src/dispatch/database/revisions/tenant/versions/2023-09-15_3538650dc471.py new file mode 100644 index 000000000000..a2358b662ce3 --- /dev/null +++ b/src/dispatch/database/revisions/tenant/versions/2023-09-15_3538650dc471.py @@ -0,0 +1,31 @@ +"""Adds type to events + +Revision ID: 3538650dc471 +Revises: e875e9544048 +Create Date: 2023-09-12 13:43:42.539336 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "3538650dc471" +down_revision = "e875e9544048" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("event", sa.Column("type", sa.String(), nullable=True)) + op.add_column("event", sa.Column("owner", sa.String(), nullable=True)) + op.add_column("event", sa.Column("pinned", sa.Boolean(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("event", "type") + op.drop_column("event", "pinned") + op.drop_column("event", "owner") + # ### end Alembic commands ### diff --git a/src/dispatch/enums.py b/src/dispatch/enums.py index 490dc6894c29..b85a532b9627 100644 --- a/src/dispatch/enums.py +++ b/src/dispatch/enums.py @@ -56,3 +56,12 @@ class DocumentResourceTemplateTypes(DispatchEnum): incident = "dispatch-incident-document-template" review = "dispatch-incident-review-document-template" tracking = "dispatch-incident-sheet-template" + + +class EventType(DispatchEnum): + other = "Other" # default and catch-all (x resource created/updated, etc.) + field_updated = "Field updated" # for fields like title, description, tags, type, etc. + assessment_updated = "Assessment updated" # for priority, status, or severity changes + participant_updated = "Participant updated" # for added/removed users and role changes + imported_message = "Imported message" # for stopwatch-reacted messages from Slack + custom_event = "Custom event" # for user-added events (new feature) diff --git a/src/dispatch/event/flows.py b/src/dispatch/event/flows.py new file mode 100644 index 000000000000..5da3ae803736 --- /dev/null +++ b/src/dispatch/event/flows.py @@ -0,0 +1,55 @@ +import logging + +from dispatch.decorators import background_task +from dispatch.event import service as event_service +from dispatch.incident import service as incident_service +from dispatch.individual import service as individual_service +from dispatch.event.models import EventUpdate, EventCreateMinimal + +log = logging.getLogger(__name__) + + +@background_task +def log_incident_event( + user_email: str, + incident_id: int, + event_in: EventCreateMinimal, + db_session=None, + organization_slug: str = None, +): + incident = incident_service.get(db_session=db_session, incident_id=incident_id) + individual = individual_service.get_by_email_and_project( + db_session=db_session, email=user_email, project_id=incident.project.id + ) + event_in.source = f"Custom event created by {individual.name}" + event_in.owner = individual.name + + event_service.log_incident_event( + db_session=db_session, + incident_id=incident_id, + **event_in.__dict__, + ) + + +@background_task +def update_incident_event( + event_in: EventUpdate, + db_session=None, + organization_slug: str = None, +): + event_service.update_incident_event( + db_session=db_session, + event_in=event_in, + ) + + +@background_task +def delete_incident_event( + event_uuid: str, + db_session=None, + organization_slug: str = None, +): + event_service.delete_incident_event( + db_session=db_session, + uuid=event_uuid, + ) diff --git a/src/dispatch/event/models.py b/src/dispatch/event/models.py index 896e397a1c89..ee96db665efb 100644 --- a/src/dispatch/event/models.py +++ b/src/dispatch/event/models.py @@ -3,12 +3,13 @@ from typing import Optional -from sqlalchemy import Column, DateTime, ForeignKey, Integer, String +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Boolean from sqlalchemy.dialects.postgresql import UUID as SQLAlchemyUUID from sqlalchemy_utils import TSVectorType, JSONType from dispatch.database.core import Base from dispatch.models import DispatchBase, TimeStampMixin +from dispatch.enums import EventType # SQLAlchemy Model @@ -21,6 +22,9 @@ class Event(Base, TimeStampMixin): source = Column(String, nullable=False) description = Column(String, nullable=False) details = Column(JSONType, nullable=True) + type = Column(String, default=EventType.other, nullable=True) + owner = Column(String, nullable=True) + pinned = Column(Boolean, default=False) # relationships individual_id = Column(Integer, ForeignKey("individual_contact.id", ondelete="CASCADE")) @@ -45,6 +49,9 @@ class EventBase(DispatchBase): source: str description: str details: Optional[dict] + type: Optional[str] + owner: Optional[str] + pinned: Optional[bool] class EventCreate(EventBase): @@ -57,3 +64,12 @@ class EventUpdate(EventBase): class EventRead(EventBase): pass + + +class EventCreateMinimal(DispatchBase): + started_at: datetime + source: str + description: str + details: dict + type: Optional[str] + owner: Optional[str] diff --git a/src/dispatch/event/service.py b/src/dispatch/event/service.py index 09d6c43c1e88..9c56ee3eaff6 100644 --- a/src/dispatch/event/service.py +++ b/src/dispatch/event/service.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from uuid import uuid4 import datetime import logging @@ -7,11 +7,12 @@ from dispatch.case import service as case_service from dispatch.incident import service as incident_service from dispatch.individual import service as individual_service +from dispatch.enums import EventType from .models import Event, EventCreate, EventUpdate -logger = logging.getLogger(__name__) +log = logging.getLogger(__name__) def get(*, db_session, event_id: int) -> Optional[Event]: @@ -19,17 +20,22 @@ def get(*, db_session, event_id: int) -> Optional[Event]: return db_session.query(Event).filter(Event.id == event_id).one_or_none() -def get_by_case_id(*, db_session, case_id: int) -> List[Optional[Event]]: +def get_by_case_id(*, db_session, case_id: int) -> list[Event | None]: """Get events by case id.""" return db_session.query(Event).filter(Event.case_id == case_id) -def get_by_incident_id(*, db_session, incident_id: int) -> List[Optional[Event]]: +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) -def get_all(*, db_session) -> List[Optional[Event]]: +def get_by_uuid(*, db_session, uuid: str) -> list[Event | None]: + """Get events by uuid.""" + return db_session.query(Event).filter(Event.uuid == uuid).one_or_none() + + +def get_all(*, db_session) -> list[Event | None]: """Get all events.""" return db_session.query(Event) @@ -71,6 +77,9 @@ def log_incident_event( started_at: datetime = None, ended_at: datetime = None, details: dict = None, + type: str = EventType.other, + owner: str = "", + pinned: bool = False, ) -> Event: """Logs an event in the incident timeline.""" uuid = uuid4() @@ -88,6 +97,9 @@ def log_incident_event( source=source, description=description, details=details, + type=type, + owner=owner, + pinned=pinned, ) event = create(db_session=db_session, event_in=event_in) @@ -133,6 +145,7 @@ def log_case_event( source=source, description=description, details=details, + type=EventType.other, ) event = create(db_session=db_session, event_in=event_in) @@ -148,3 +161,24 @@ def log_case_event( db_session.commit() return event + + +def update_incident_event( + db_session, + event_in: EventUpdate, +) -> Event: + """Updates an event in the incident timeline.""" + event = get_by_uuid(db_session=db_session, uuid=event_in.uuid) + event = update(db_session=db_session, event=event, event_in=event_in) + + return event + + +def delete_incident_event( + db_session, + uuid: str, +): + """Deletes an event.""" + event = get_by_uuid(db_session=db_session, uuid=uuid) + + delete(db_session=db_session, event_id=event.id) diff --git a/src/dispatch/incident/flows.py b/src/dispatch/incident/flows.py index 4585f5f5d4d6..8976eec03aec 100644 --- a/src/dispatch/incident/flows.py +++ b/src/dispatch/incident/flows.py @@ -10,7 +10,7 @@ from dispatch.decorators import background_task from dispatch.document import flows as document_flows from dispatch.enums import DocumentResourceTypes -from dispatch.enums import Visibility +from dispatch.enums import Visibility, EventType from dispatch.event import service as event_service from dispatch.group import flows as group_flows from dispatch.group.enums import GroupType, GroupAction @@ -77,6 +77,7 @@ def get_incident_participants( source=plugin.plugin.title, description="Incident participants resolved", incident_id=incident.id, + type=EventType.participant_updated, ) else: event_service.log_incident_event( @@ -84,6 +85,7 @@ def get_incident_participants( source="Dispatch Core App", description="Incident participants not resolved", incident_id=incident.id, + type=EventType.participant_updated, ) log.warning("Incident participants not resolved. No participant plugin enabled.") @@ -104,6 +106,7 @@ def reactivate_incident_participants(incident: Incident, db_session: Session): source="Dispatch Core App", description=f"Unable to reactivate participant with email {participant.individual.email}", incident_id=incident.id, + type=EventType.participant_updated, ) log.exception(e) @@ -112,6 +115,7 @@ def reactivate_incident_participants(incident: Incident, db_session: Session): source="Dispatch Core App", description="Incident participants reactivated", incident_id=incident.id, + type=EventType.participant_updated, ) @@ -129,6 +133,7 @@ def inactivate_incident_participants(incident: Incident, db_session: Session): source="Dispatch Core App", description=f"Unable to inactivate participant with email {participant.individual.email}", incident_id=incident.id, + type=EventType.participant_updated, ) log.exception(e) @@ -137,6 +142,7 @@ def inactivate_incident_participants(incident: Incident, db_session: Session): source="Dispatch Core App", description="Incident participants inactivated", incident_id=incident.id, + type=EventType.participant_updated, ) @@ -276,6 +282,7 @@ def incident_create_resources(*, incident: Incident, db_session=None) -> Inciden source="Dispatch Core App", description="Incident participants added to incident", incident_id=incident.id, + type=EventType.participant_updated, ) return incident @@ -481,6 +488,8 @@ def conversation_topic_dispatcher( description=f'{individual.name} changed the incident title to "{incident.title}"', incident_id=incident.id, individual_id=individual.id, + type=EventType.field_updated, + owner=individual.name, ) if previous_incident.description != incident.description: @@ -491,6 +500,8 @@ def conversation_topic_dispatcher( details={"description": incident.description}, incident_id=incident.id, individual_id=individual.id, + type=EventType.field_updated, + owner=individual.name, ) description, details = check_for_tag_change( @@ -504,6 +515,8 @@ def conversation_topic_dispatcher( details=details, incident_id=incident.id, individual_id=individual.id, + type=EventType.field_updated, + owner=individual.name, ) if previous_incident.incident_type.name != incident.incident_type.name: @@ -515,6 +528,8 @@ def conversation_topic_dispatcher( description=f"{individual.name} changed the incident type to {incident.incident_type.name}", incident_id=incident.id, individual_id=individual.id, + type=EventType.field_updated, + owner=individual.name, ) if previous_incident.incident_severity.name != incident.incident_severity.name: @@ -526,6 +541,8 @@ def conversation_topic_dispatcher( description=f"{individual.name} changed the incident severity to {incident.incident_severity.name}", incident_id=incident.id, individual_id=individual.id, + type=EventType.assessment_updated, + owner=individual.name, ) if previous_incident.incident_priority.name != incident.incident_priority.name: @@ -537,6 +554,8 @@ def conversation_topic_dispatcher( description=f"{individual.name} changed the incident priority to {incident.incident_priority.name}", incident_id=incident.id, individual_id=individual.id, + type=EventType.assessment_updated, + owner=individual.name, ) if previous_incident.status != incident.status: @@ -548,6 +567,8 @@ def conversation_topic_dispatcher( description=f"{individual.name} marked the incident as {incident.status.lower()}", incident_id=incident.id, individual_id=individual.id, + type=EventType.assessment_updated, + owner=individual.name, ) if conversation_topic_change: @@ -597,6 +618,7 @@ def status_flow_dispatcher( 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, ) diff --git a/src/dispatch/incident/service.py b/src/dispatch/incident/service.py index 5c6a70daf5af..c7838fd30bb5 100644 --- a/src/dispatch/incident/service.py +++ b/src/dispatch/incident/service.py @@ -193,6 +193,8 @@ def create(*, db_session, incident_in: IncidentCreate) -> Incident: db_session.add(incident) db_session.commit() + reporter_name = incident_in.reporter.individual.name if incident_in.reporter else "" + event_service.log_incident_event( db_session=db_session, source="Dispatch Core App", @@ -207,6 +209,8 @@ def create(*, db_session, incident_in: IncidentCreate) -> Incident: "visibility": incident.visibility, }, incident_id=incident.id, + owner=reporter_name, + pinned=True, ) # add reporter diff --git a/src/dispatch/incident/views.py b/src/dispatch/incident/views.py index f8c740a70230..d20d3bcdbb4f 100644 --- a/src/dispatch/incident/views.py +++ b/src/dispatch/incident/views.py @@ -1,7 +1,7 @@ import calendar import json import logging -from datetime import date +from datetime import date, datetime from typing import Annotated, List from dateutil.relativedelta import relativedelta @@ -13,6 +13,7 @@ from dispatch.auth.permissions import ( IncidentEditPermission, + IncidentCommanderOrScribePermission, IncidentJoinOrSubscribePermission, IncidentViewPermission, PermissionsDependency, @@ -26,7 +27,9 @@ from dispatch.models import OrganizationSlug, PrimaryKey from dispatch.participant.models import ParticipantUpdate from dispatch.report import flows as report_flows +from dispatch.event import flows as event_flows from dispatch.report.models import ExecutiveReportCreate, TacticalReportCreate +from dispatch.event.models import EventUpdate, EventCreateMinimal from .flows import ( incident_add_or_reactivate_participant_flow, @@ -320,6 +323,85 @@ def create_executive_report( ) +@router.post( + "/{incident_id}/event", + summary="Creates a custom event.", + dependencies=[Depends(PermissionsDependency([IncidentEditPermission]))], +) +def create_custom_event( + db_session: DbSession, + organization: OrganizationSlug, + incident_id: PrimaryKey, + current_incident: CurrentIncident, + event_in: EventCreateMinimal, + current_user: CurrentUser, + background_tasks: BackgroundTasks, +): + 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_incident_event, + user_email=current_user.email, + incident_id=current_incident.id, + event_in=event_in, + organization_slug=organization, + ) + + +@router.patch( + "/{incident_id}/event", + summary="Updates a custom event.", + dependencies=[Depends(PermissionsDependency([IncidentCommanderOrScribePermission]))], +) +def update_custom_event( + db_session: DbSession, + organization: OrganizationSlug, + incident_id: PrimaryKey, + current_incident: CurrentIncident, + 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_incident_event, + event_in=event_in, + organization_slug=organization, + ) + + +@router.delete( + "/{incident_id}/event/{event_uuid}", + summary="Deletes a custom event.", + dependencies=[Depends(PermissionsDependency([IncidentCommanderOrScribePermission]))], +) +def delete_custom_event( + db_session: DbSession, + organization: OrganizationSlug, + incident_id: PrimaryKey, + current_incident: CurrentIncident, + event_uuid: str, + current_user: CurrentUser, + background_tasks: BackgroundTasks, +): + """Deletes a custom event.""" + background_tasks.add_task( + event_flows.delete_incident_event, + event_uuid=event_uuid, + organization_slug=organization, + ) + + def get_month_range(relative): today = date.today() relative_month = today - relativedelta(months=relative) diff --git a/src/dispatch/participant/flows.py b/src/dispatch/participant/flows.py index 04b5dd56191a..6c056d2de885 100644 --- a/src/dispatch/participant/flows.py +++ b/src/dispatch/participant/flows.py @@ -14,6 +14,7 @@ ) from dispatch.participant import service as participant_service from dispatch.service import service as service_service +from dispatch.enums import EventType log = logging.getLogger(__name__) @@ -85,6 +86,7 @@ def add_participant( source="Dispatch Core App", description=f"{individual.name} added to incident with {participant_role.role} role", incident_id=subject.id, + type=EventType.participant_updated, ) return participant @@ -111,6 +113,7 @@ def remove_participant(user_email: str, incident: Incident, db_session: SessionL source="Dispatch Core App", description=f"{participant.individual.name} has been removed", incident_id=incident.id, + type=EventType.participant_updated, ) @@ -141,6 +144,7 @@ def inactivate_participant(user_email: str, incident: Incident, db_session: Sess source="Dispatch Core App", description=f"{participant.individual.name} has been inactivated", incident_id=incident.id, + type=EventType.participant_updated, ) return True @@ -183,6 +187,7 @@ def reactivate_participant( source="Dispatch Core App", description=f"{participant.individual.name} has been reactivated", incident_id=incident.id, + type=EventType.participant_updated, ) return True diff --git a/src/dispatch/participant_role/flows.py b/src/dispatch/participant_role/flows.py index 652e1c3c9ff0..434ec9526860 100644 --- a/src/dispatch/participant_role/flows.py +++ b/src/dispatch/participant_role/flows.py @@ -5,6 +5,7 @@ from dispatch.event import service as event_service from dispatch.participant import service as participant_service from dispatch.participant_role.models import ParticipantRoleType +from dispatch.enums import EventType from .service import get_all_active_roles, add_role, renounce_role @@ -58,6 +59,7 @@ def assign_role_flow( source="Dispatch Core App", description=f"{assignee_participant.individual.name} has been assigned the role of {assignee_role}", incident_id=subject.id, + type=EventType.participant_updated, ) return "role_assigned" @@ -143,6 +145,7 @@ def assign_role_flow( source="Dispatch Core App", description=f"{assignee_participant.individual.name} has been assigned the role of {assignee_role}", incident_id=subject.id, + type=EventType.participant_updated, ) if subject_type == "case": event_service.log_case_event( diff --git a/src/dispatch/plugins/dispatch_slack/fields.py b/src/dispatch/plugins/dispatch_slack/fields.py index 169c1ec6aa8e..0ffe595c4d47 100644 --- a/src/dispatch/plugins/dispatch_slack/fields.py +++ b/src/dispatch/plugins/dispatch_slack/fields.py @@ -222,10 +222,25 @@ def datetime_picker_block( **kwargs, ): """Builds a datetime picker block""" + hour = None + minute = None + date = initial_option.split("|")[0] if initial_option.split("|")[0] != "" else None + + if initial_option.split("|")[1] != "": + # appends zero if time is not entered in hh format + if len(initial_option.split("|")[1].split(":")[0]) == 1: + h = "0" + initial_option.split("|")[1].split(":")[0] + else: + h = initial_option.split("|")[1].split(":")[0] + hour = {"text": h, "value": h} + minute = { + "text": initial_option.split("|")[1].split(":")[1], + "value": initial_option.split("|")[1].split(":")[1], + } return [ - date_picker_input(), - hour_picker_input(), - minute_picker_input(), + date_picker_input(initial_date=date), + hour_picker_input(initial_option=hour), + minute_picker_input(initial_option=minute), timezone_picker_input(), ] diff --git a/src/dispatch/plugins/dispatch_slack/incident/interactive.py b/src/dispatch/plugins/dispatch_slack/incident/interactive.py index f8718e7097fa..509bd083eb3a 100644 --- a/src/dispatch/plugins/dispatch_slack/incident/interactive.py +++ b/src/dispatch/plugins/dispatch_slack/incident/interactive.py @@ -1,4 +1,5 @@ import logging +import re import uuid from datetime import datetime, timedelta from typing import Any @@ -28,7 +29,7 @@ from dispatch.auth.models import DispatchUser from dispatch.config import DISPATCH_UI_URL from dispatch.database.service import search_filter_sort_paginate -from dispatch.enums import Visibility +from dispatch.enums import Visibility, EventType from dispatch.event import service as event_service from dispatch.exceptions import DispatchException from dispatch.group import flows as group_flows @@ -686,11 +687,13 @@ def handle_timeline_added_event( # we log the event event_service.log_incident_event( db_session=db_session, - source="Slack Plugin - Conversation Management", + source=f"Slack message from {individual.name}", description=f'"{message_text}," said {individual.name}', incident_id=context["subject"].id, individual_id=individual.id, started_at=message_ts_utc, + type=EventType.imported_message, + owner=individual.name, ) @@ -735,6 +738,7 @@ def handle_participant_role_activity( f"{ParticipantRoleType.participant} due to activity in the incident channel" ), incident_id=context["subject"].id, + type=EventType.participant_updated, ) db_session.commit() @@ -972,6 +976,31 @@ def handle_add_timeline_event_command( ) -> None: """Handles the add timeline event command.""" ack() + description = None + date = "" + time = "" + if re.match(".*DESC\\s*(.+?)(?: DATE|$|TIME)", body["text"], re.IGNORECASE): + description = ( + re.match("DESC\\s*(.+?)(?: DATE|$|TIME)", body["text"], re.IGNORECASE).group(1) + ).strip() + if re.match( + ".*DATE\\s*(\\d{4}\\-\\d{2}\\-\\d{2})(?: TIME|$|DESC)", body["text"], re.IGNORECASE + ): + date = ( + re.match( + ".*DATE\\s*(\\d{4}\\-\\d{2}\\-\\d{2})(?: TIME|$|DESC)", body["text"], re.IGNORECASE + ).group(1) + ).strip() + if re.match( + ".*TIME\\s*(([01]?[0-9]|2[0-3]):[0-5][0-9])(?: |DATE|$|DESC)", body["text"], re.IGNORECASE + ): + time = ( + re.match( + ".*TIME\\s*(([01]?[0-9]|2[0-3]):[0-5][0-9])(?: |DATE|$|DESC)", + body["text"], + re.IGNORECASE, + ).group(1) + ).strip() blocks = [ Context( @@ -979,10 +1008,10 @@ def handle_add_timeline_event_command( MarkdownText(text="Use this form to add an event to the incident's timeline.") ] ), - description_input(), + description_input(initial_value=description), ] - blocks.extend(datetime_picker_block()) + blocks.extend(datetime_picker_block(initial_option=date + "|" + time)) modal = Modal( title="Add Timeline Event", @@ -1041,11 +1070,13 @@ def handle_add_timeline_submission_event( event_service.log_incident_event( db_session=db_session, - source="Slack Plugin - Conversation Management", + source=f"Slack message from {participant.individual.name}", started_at=event_dt_utc, description=f'"{event_description}," said {participant.individual.name}', incident_id=context["subject"].id, individual_id=participant.individual.id, + type=EventType.imported_message, + owner=participant.individual.name, ) send_success_modal( diff --git a/src/dispatch/static/dispatch/package-lock.json b/src/dispatch/static/dispatch/package-lock.json index dadfccd0d4e6..28a7a5b2fb99 100644 --- a/src/dispatch/static/dispatch/package-lock.json +++ b/src/dispatch/static/dispatch/package-lock.json @@ -4518,9 +4518,15 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -4850,9 +4856,9 @@ } }, "node_modules/postcss": { - "version": "8.4.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", - "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "funding": [ { "type": "opencollective", @@ -4861,10 +4867,14 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -10090,9 +10100,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" }, "natural-compare": { "version": "1.4.0", @@ -10316,11 +10326,11 @@ "dev": true }, "postcss": { - "version": "8.4.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", - "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "requires": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } diff --git a/src/dispatch/static/dispatch/src/components/DateTimePickerMenu.vue b/src/dispatch/static/dispatch/src/components/DateTimePickerMenu.vue index cac5aa52aa6d..98f6b5286ebb 100644 --- a/src/dispatch/static/dispatch/src/components/DateTimePickerMenu.vue +++ b/src/dispatch/static/dispatch/src/components/DateTimePickerMenu.vue @@ -51,7 +51,7 @@ diff --git a/src/dispatch/static/dispatch/src/entity/EntityFilterCombobox.vue b/src/dispatch/static/dispatch/src/entity/EntityFilterCombobox.vue index 25c1bbf5c520..5714fa75b7af 100644 --- a/src/dispatch/static/dispatch/src/entity/EntityFilterCombobox.vue +++ b/src/dispatch/static/dispatch/src/entity/EntityFilterCombobox.vue @@ -55,7 +55,7 @@ import SearchUtils from "@/search/utils" import EntityApi from "@/entity/api" export default { - name: "EntityCombobox", + name: "EntityFilterCombobox", props: { value: { @@ -64,7 +64,7 @@ export default { }, label: { type: String, - default: "Add Entity Types", + default: "Add Entities", }, model: { type: String, diff --git a/src/dispatch/static/dispatch/src/entity_type/EntityTypeFilterCombobox.vue b/src/dispatch/static/dispatch/src/entity_type/EntityTypeFilterCombobox.vue index f21795b84571..211c8ca8458f 100644 --- a/src/dispatch/static/dispatch/src/entity_type/EntityTypeFilterCombobox.vue +++ b/src/dispatch/static/dispatch/src/entity_type/EntityTypeFilterCombobox.vue @@ -118,7 +118,7 @@ export default { }, label: { type: String, - default: "Add Entity Types", + default: "Add Entity Type Filters", }, model: { type: String, @@ -198,7 +198,7 @@ export default { ...filterOptions, filters: { project: [this.project], - scope: ["multiple"], + scope: ["multiple", "single"], }, } filterOptions = SearchUtils.createParametersFromTableOptions({ ...filterOptions }) diff --git a/src/dispatch/static/dispatch/src/incident/DeleteEventDialog.vue b/src/dispatch/static/dispatch/src/incident/DeleteEventDialog.vue new file mode 100644 index 000000000000..9df60d5bae9f --- /dev/null +++ b/src/dispatch/static/dispatch/src/incident/DeleteEventDialog.vue @@ -0,0 +1,37 @@ + + + diff --git a/src/dispatch/static/dispatch/src/incident/EditEventDialog.vue b/src/dispatch/static/dispatch/src/incident/EditEventDialog.vue new file mode 100644 index 000000000000..722d7c40db06 --- /dev/null +++ b/src/dispatch/static/dispatch/src/incident/EditEventDialog.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/src/dispatch/static/dispatch/src/incident/EditSheet.vue b/src/dispatch/static/dispatch/src/incident/EditSheet.vue index 1031975dbea7..e0970353618b 100644 --- a/src/dispatch/static/dispatch/src/incident/EditSheet.vue +++ b/src/dispatch/static/dispatch/src/incident/EditSheet.vue @@ -1,6 +1,12 @@