diff --git a/requirements-base.txt b/requirements-base.txt
index e7f2d7377c99..0775b5d9bc6e 100644
--- a/requirements-base.txt
+++ b/requirements-base.txt
@@ -114,7 +114,7 @@ email-validator==2.0.0.post2
# via -r requirements-base.in
emails==0.6
# via -r requirements-base.in
-fastapi==0.103.1
+fastapi==0.103.2
# via -r requirements-base.in
frozenlist==1.4.0
# via
@@ -122,7 +122,7 @@ frozenlist==1.4.0
# aiosignal
google-api-core==2.11.1
# via google-api-python-client
-google-api-python-client==2.100.0
+google-api-python-client==2.101.0
# via -r requirements-base.in
google-auth==2.22.0
# via
@@ -207,7 +207,7 @@ markupsafe==2.1.3
# jinja2
# mako
# werkzeug
-msal==1.24.0
+msal==1.24.1
# via -r requirements-base.in
multidict==6.0.4
# via
@@ -235,7 +235,7 @@ oauthlib[signedtoken]==3.2.2
# atlassian-python-api
# jira
# requests-oauthlib
-openai==0.28.0
+openai==0.28.1
# via -r requirements-base.in
packaging==23.1
# via
@@ -272,7 +272,7 @@ protobuf==4.24.3
# -r requirements-base.in
# google-api-core
# googleapis-common-protos
-psycopg2-binary==2.9.7
+psycopg2-binary==2.9.8
# via -r requirements-base.in
pyasn1==0.5.0
# via
@@ -286,7 +286,7 @@ pyasn1-modules==0.3.0
# oauth2client
pycparser==2.21
# via cffi
-pydantic==1.10.12
+pydantic==1.10.13
# via
# -r requirements-base.in
# blockkit
diff --git a/requirements-dev.txt b/requirements-dev.txt
index d15fe4be7bbd..c8d639b2ce32 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -44,7 +44,7 @@ identify==2.5.27
# via pre-commit
iniconfig==2.0.0
# via pytest
-ipython==8.15.0
+ipython==8.16.0
# via -r requirements-dev.in
jedi==0.19.0
# via ipython
@@ -90,7 +90,7 @@ python-dateutil==2.8.2
# via faker
pyyaml==6.0.1
# via pre-commit
-ruff==0.0.290
+ruff==0.0.291
# via -r requirements-dev.in
six==1.16.0
# via
diff --git a/src/dispatch/api.py b/src/dispatch/api.py
index 882a08d99b4d..21c84e50de7d 100644
--- a/src/dispatch/api.py
+++ b/src/dispatch/api.py
@@ -24,6 +24,7 @@
from dispatch.entity.views import router as entity_router
from dispatch.entity_type.views import router as entity_type_router
from dispatch.feedback.incident.views import router as feedback_router
+from dispatch.feedback.service.views import router as service_feedback_router
from dispatch.incident.priority.views import router as incident_priority_router
from dispatch.incident.severity.views import router as incident_severity_router
from dispatch.incident.type.views import router as incident_type_router
@@ -203,6 +204,9 @@ def get_organization_path(organization: OrganizationSlug):
authenticated_organization_api_router.include_router(
feedback_router, prefix="/feedback", tags=["feedback"]
)
+authenticated_organization_api_router.include_router(
+ service_feedback_router, prefix="/service_feedback", tags=["service_feedback"]
+)
authenticated_organization_api_router.include_router(
notification_router, prefix="/notifications", tags=["notifications"]
)
diff --git a/src/dispatch/case/flows.py b/src/dispatch/case/flows.py
index 4f64d6ee717c..6c06cb88113d 100644
--- a/src/dispatch/case/flows.py
+++ b/src/dispatch/case/flows.py
@@ -6,8 +6,7 @@
from dispatch.case import service as case_service
from dispatch.case.models import CaseRead
-from dispatch.conversation import service as conversation_service
-from dispatch.conversation.models import ConversationCreate
+from dispatch.conversation import flows as conversation_flows
from dispatch.database.core import SessionLocal
from dispatch.decorators import background_task
from dispatch.document import flows as document_flows
@@ -154,26 +153,6 @@ def case_add_or_reactivate_participant_flow(
return participant
-def create_conversation(case: Case, conversation_target: str, db_session: SessionLocal):
- """Create external communication conversation."""
- plugin = plugin_service.get_active_instance(
- db_session=db_session, project_id=case.project.id, plugin_type="conversation"
- )
- conversation = plugin.instance.create_threaded(
- case=case, conversation_id=conversation_target, db_session=db_session
- )
- conversation.update({"resource_type": plugin.plugin.slug, "resource_id": conversation["id"]})
-
- event_service.log_case_event(
- db_session=db_session,
- source=plugin.plugin.title,
- description="Case conversation created",
- case_id=case.id,
- )
-
- return conversation
-
-
def update_conversation(case: Case, db_session: SessionLocal):
"""Updates external communication conversation."""
plugin = plugin_service.get_active_instance(
@@ -217,6 +196,7 @@ def case_new_create_flow(
case_id=case.id,
individual_participants=individual_participants,
team_participants=team_participants,
+ conversation_target=conversation_target,
)
if case.case_priority.page_assignee:
@@ -241,67 +221,6 @@ def case_new_create_flow(
else:
log.warning("Case assignee not paged. No plugin of type oncall enabled.")
- conversation_plugin = plugin_service.get_active_instance(
- db_session=db_session, project_id=case.project.id, plugin_type="conversation"
- )
- if conversation_plugin:
- if not conversation_target:
- conversation_target = case.case_type.conversation_target
- if conversation_target:
- try:
- # TODO: Refactor conversation creation using conversation_flows module
- conversation = create_conversation(case, conversation_target, db_session)
- conversation_in = ConversationCreate(
- resource_id=conversation["resource_id"],
- resource_type=conversation["resource_type"],
- weblink=conversation["weblink"],
- thread_id=conversation["timestamp"],
- channel_id=conversation["id"],
- )
- case.conversation = conversation_service.create(
- db_session=db_session, conversation_in=conversation_in
- )
-
- event_service.log_case_event(
- db_session=db_session,
- source="Dispatch Core App",
- description="Conversation added to case",
- case_id=case.id,
- )
- # wait until all resources are created before adding suggested participants
- individual_participants = [x.email for x, _ in individual_participants]
-
- for email in individual_participants:
- # we don't rely on on this flow to add folks to the conversation because in this case
- # we want to do it in bulk
- case_add_or_reactivate_participant_flow(
- db_session=db_session,
- user_email=email,
- case_id=case.id,
- add_to_conversation=False,
- )
- # explicitly add the assignee to the conversation
- all_participants = individual_participants + [case.assignee.individual.email]
- conversation_plugin.instance.add_to_thread(
- case.conversation.channel_id,
- case.conversation.thread_id,
- all_participants,
- )
- event_service.log_case_event(
- db_session=db_session,
- source="Dispatch Core App",
- description="Case participants added to conversation.",
- case_id=case.id,
- )
- except Exception as e:
- event_service.log_case_event(
- db_session=db_session,
- source="Dispatch Core App",
- description=f"Creation of case conversation failed. Reason: {e}",
- case_id=case.id,
- )
- log.exception(e)
-
db_session.add(case)
db_session.commit()
@@ -684,7 +603,12 @@ def case_assign_role_flow(
def case_create_resources_flow(
- db_session: Session, case_id: int, individual_participants: list, team_participants: list
+ db_session: Session,
+ case_id: int,
+ individual_participants: List[str],
+ team_participants: List[str],
+ conversation_target: str = None,
+ create_resources: bool = True,
) -> None:
"""Runs the case resource creation flow."""
case = get(db_session=db_session, case_id=case_id)
@@ -692,43 +616,93 @@ def case_create_resources_flow(
if case.assignee:
individual_participants.append((case.assignee.individual, None))
- # we create the tactical group
- direct_participant_emails = [i.email for i, _ in individual_participants]
+ if create_resources:
+ # we create the tactical group
+ direct_participant_emails = [i.email for i, _ in individual_participants]
- indirect_participant_emails = [t.email for t in team_participants]
+ indirect_participant_emails = [t.email for t in team_participants]
- group = group_flows.create_group(
- subject=case,
- group_type=GroupType.tactical,
- group_participants=list(set(direct_participant_emails + indirect_participant_emails)),
- db_session=db_session,
- )
+ if not case.groups:
+ group_flows.create_group(
+ subject=case,
+ group_type=GroupType.tactical,
+ group_participants=list(
+ set(direct_participant_emails + indirect_participant_emails)
+ ),
+ db_session=db_session,
+ )
- # we create the storage folder
- storage_members = []
- if group:
- storage_members = [group.email]
+ # we create the storage folder
+ storage_members = []
+ if case.tactical_group:
+ storage_members = [case.tactical_group.email]
+ # direct add members if not group exists
+ else:
+ storage_members = direct_participant_emails
- # direct add members if not group exists
- else:
- storage_members = direct_participant_emails
+ if not case.storage:
+ storage_flows.create_storage(
+ subject=case, storage_members=storage_members, db_session=db_session
+ )
- case.storage = storage_flows.create_storage(
- subject=case, storage_members=storage_members, db_session=db_session
- )
+ # we create the investigation document
+ if not case.case_document:
+ document_flows.create_document(
+ subject=case,
+ document_type=DocumentResourceTypes.case,
+ document_template=case.case_type.case_template_document,
+ db_session=db_session,
+ )
- # we create the investigation document
- document = document_flows.create_document(
- subject=case,
- document_type=DocumentResourceTypes.case,
- document_template=case.case_type.case_template_document,
- db_session=db_session,
- )
+ # we update the ticket
+ ticket_flows.update_case_ticket(case=case, db_session=db_session)
- # we update the ticket
- ticket_flows.update_case_ticket(case=case, db_session=db_session)
+ # we update the case document
+ document_flows.update_document(
+ document=case.case_document, project_id=case.project.id, db_session=db_session
+ )
- # we update the case document
- document_flows.update_document(
- document=document, project_id=case.project.id, db_session=db_session
- )
+ try:
+ # we create the conversation and add participants to the thread
+ conversation_flows.create_case_conversation(case, conversation_target, db_session)
+
+ event_service.log_case_event(
+ db_session=db_session,
+ source="Dispatch Core App",
+ description="Conversation added to case",
+ case_id=case.id,
+ )
+ # wait until all resources are created before adding suggested participants
+ individual_participants = [x.email for x, _ in individual_participants]
+
+ for email in individual_participants:
+ # we don't rely on on this flow to add folks to the conversation because in this case
+ # we want to do it in bulk
+ case_add_or_reactivate_participant_flow(
+ db_session=db_session,
+ user_email=email,
+ case_id=case.id,
+ add_to_conversation=False,
+ )
+ # explicitly add the assignee to the conversation
+ all_participants = individual_participants + [case.assignee.individual.email]
+
+ # # we add the participant to the conversation
+ conversation_flows.add_case_participants(
+ case=case, participant_emails=all_participants, db_session=db_session
+ )
+
+ event_service.log_case_event(
+ db_session=db_session,
+ source="Dispatch Core App",
+ description="Case participants added to conversation.",
+ case_id=case.id,
+ )
+ except Exception as e:
+ event_service.log_case_event(
+ db_session=db_session,
+ source="Dispatch Core App",
+ description=f"Creation of case conversation failed. Reason: {e}",
+ case_id=case.id,
+ )
+ log.exception(e)
diff --git a/src/dispatch/case/views.py b/src/dispatch/case/views.py
index 1d9e68f36e94..add6f8a5037a 100644
--- a/src/dispatch/case/views.py
+++ b/src/dispatch/case/views.py
@@ -34,6 +34,8 @@
case_new_create_flow,
case_triage_create_flow,
case_update_flow,
+ case_create_resources_flow,
+ get_case_participants,
)
from .models import Case, CaseCreate, CasePagination, CaseRead, CaseUpdate, CaseExpandedPagination
from .service import create, delete, get, update
@@ -145,6 +147,32 @@ def create_case(
return case
+@router.post(
+ "/{case_id}/resources",
+ response_model=CaseRead,
+ summary="Creates resources for an existing case.",
+)
+def create_case_resources(
+ db_session: DbSession,
+ case_id: PrimaryKey,
+ current_case: CurrentCase,
+ background_tasks: BackgroundTasks,
+):
+ """Creates resources for an existing case."""
+ individual_participants, team_participants = get_case_participants(
+ case=current_case, db_session=db_session
+ )
+ background_tasks.add_task(
+ case_create_resources_flow,
+ db_session=db_session,
+ case_id=case_id,
+ individual_participants=individual_participants,
+ team_participants=team_participants,
+ )
+
+ return current_case
+
+
@router.put(
"/{case_id}",
response_model=CaseRead,
diff --git a/src/dispatch/conversation/flows.py b/src/dispatch/conversation/flows.py
index ab14fbf4b4a3..dadba50ff470 100644
--- a/src/dispatch/conversation/flows.py
+++ b/src/dispatch/conversation/flows.py
@@ -2,6 +2,7 @@
from typing import TypeVar, List
+from dispatch.case.models import Case
from dispatch.conference.models import Conference
from dispatch.database.core import SessionLocal, resolve_attr
from dispatch.document.models import Document
@@ -22,7 +23,57 @@
Resource = TypeVar("Resource", Document, Conference, Storage, Ticket)
-def create_conversation(incident: Incident, db_session: SessionLocal):
+def create_case_conversation(case: Case, conversation_target: str, db_session: SessionLocal):
+ """Create external communication conversation."""
+
+ plugin = plugin_service.get_active_instance(
+ db_session=db_session, project_id=case.project.id, plugin_type="conversation"
+ )
+ if not plugin:
+ log.warning("Conversation not created. No conversation plugin enabled.")
+ return
+
+ if not conversation_target:
+ conversation_target = case.case_type.conversation_target
+
+ if conversation_target:
+ try:
+ conversation = plugin.instance.create_threaded(
+ case=case, conversation_id=conversation_target, db_session=db_session
+ )
+ except Exception as e:
+ # TODO: consistency across exceptions
+ log.exception(e)
+
+ if not conversation:
+ log.error(f"Conversation not created. Plugin {plugin.plugin.slug} encountered an error.")
+ return
+
+ conversation.update({"resource_type": plugin.plugin.slug, "resource_id": conversation["id"]})
+
+ conversation_in = ConversationCreate(
+ resource_id=conversation["resource_id"],
+ resource_type=conversation["resource_type"],
+ weblink=conversation["weblink"],
+ thread_id=conversation["timestamp"],
+ channel_id=conversation["id"],
+ )
+ case.conversation = create(db_session=db_session, conversation_in=conversation_in)
+
+ event_service.log_case_event(
+ db_session=db_session,
+ source=plugin.plugin.title,
+ description="Case conversation created",
+ case_id=case.id,
+ )
+
+ db_session.add(case)
+ db_session.commit()
+
+ return case.conversation
+
+
+def create_incident_conversation(incident: Incident, db_session: SessionLocal):
"""Creates a conversation."""
plugin = plugin_service.get_active_instance(
db_session=db_session, project_id=incident.project.id, plugin_type="conversation"
@@ -59,8 +110,7 @@ def create_conversation(incident: Incident, db_session: SessionLocal):
weblink=external_conversation["weblink"],
channel_id=external_conversation["id"],
)
- conversation = create(conversation_in=conversation_in, db_session=db_session)
- incident.conversation = conversation
+ incident.conversation = create(conversation_in=conversation_in, db_session=db_session)
db_session.add(incident)
db_session.commit()
@@ -72,7 +122,7 @@ def create_conversation(incident: Incident, db_session: SessionLocal):
incident_id=incident.id,
)
- return conversation
+ return incident.conversation
def archive_conversation(incident: Incident, db_session: SessionLocal):
@@ -262,8 +312,43 @@ def add_conversation_bookmarks(incident: Incident, db_session: SessionLocal):
log.exception(e)
-def add_participants(incident: Incident, participant_emails: List[str], db_session: SessionLocal):
- """Adds one or more participants to the conversation."""
+def add_case_participants(case: Case, participant_emails: List[str], db_session: SessionLocal):
+ """Adds one or more participants to the case conversation."""
+ if not case.conversation:
+ log.warning(
+ "Case participant(s) not added to conversation. No conversation available for this case."
+ )
+ return
+
+ plugin = plugin_service.get_active_instance(
+ db_session=db_session, project_id=case.project.id, plugin_type="conversation"
+ )
+ if not plugin:
+ log.warning(
+ "Case participant(s) not added to conversation. No conversation plugin enabled."
+ )
+ return
+
+ try:
+ plugin.instance.add_to_thread(
+ case.conversation.channel_id,
+ case.conversation.thread_id,
+ participant_emails,
+ )
+ except Exception as e:
+ event_service.log_case_event(
+ db_session=db_session,
+ source="Dispatch Core App",
+ description=f"Adding participant(s) to case conversation failed. Reason: {e}",
+ case_id=case.id,
+ )
+ log.exception(e)
+
+
+def add_incident_participants(
+ incident: Incident, participant_emails: List[str], db_session: SessionLocal
+):
+ """Adds one or more participants to the incident conversation."""
if not incident.conversation:
log.warning(
"Incident participant(s) not added to conversation. No conversation available for this incident."
diff --git a/src/dispatch/database/service.py b/src/dispatch/database/service.py
index 03a3c8c39d7a..e404b96a7d7b 100644
--- a/src/dispatch/database/service.py
+++ b/src/dispatch/database/service.py
@@ -504,12 +504,15 @@ def search_filter_sort_paginate(
sort = False if sort_by else True
query = search(query_str=query_str, query=query, model=model, sort=sort)
- query = apply_model_specific_filters(model_cls, query, current_user, role)
+ query_restricted = apply_model_specific_filters(model_cls, query, current_user, role)
if filter_spec:
query = apply_filter_specific_joins(model_cls, filter_spec, query)
query = apply_filters(query, filter_spec, model_cls)
+ if model == "Incident":
+ query = query.intersect(query_restricted)
+
if sort_by:
sort_spec = create_sort_spec(model, sort_by, descending)
query = apply_sort(query, sort_spec)
diff --git a/src/dispatch/feedback/service/models.py b/src/dispatch/feedback/service/models.py
index e1d09e9f51ca..e2773f347bac 100644
--- a/src/dispatch/feedback/service/models.py
+++ b/src/dispatch/feedback/service/models.py
@@ -5,10 +5,11 @@
from sqlalchemy import Column, Integer, ForeignKey, DateTime, String
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy_utils import TSVectorType
+from sqlalchemy.orm import relationship
from dispatch.database.core import Base
-from dispatch.individual.models import IndividualContactRead
-from dispatch.models import DispatchBase, TimeStampMixin, FeedbackMixin, PrimaryKey
+from dispatch.individual.models import IndividualContactReadMinimal
+from dispatch.models import DispatchBase, TimeStampMixin, FeedbackMixin, PrimaryKey, Pagination
from dispatch.project.models import ProjectRead
from .enums import ServiceFeedbackRating
@@ -26,6 +27,7 @@ class ServiceFeedback(TimeStampMixin, FeedbackMixin, Base):
# Relationships
individual_contact_id = Column(Integer, ForeignKey("individual_contact.id"))
+ individual = relationship("IndividualContact")
search_vector = Column(
TSVectorType(
@@ -37,14 +39,14 @@ class ServiceFeedback(TimeStampMixin, FeedbackMixin, Base):
@hybrid_property
def project(self):
- return self.service.project
+ return self.individual.project
# Pydantic models
class ServiceFeedbackBase(DispatchBase):
feedback: Optional[str] = Field(None, nullable=True)
hours: Optional[int]
- individual: Optional[IndividualContactRead]
+ individual: Optional[IndividualContactReadMinimal]
rating: ServiceFeedbackRating = ServiceFeedbackRating.little_effort
schedule: Optional[str]
shift_end_at: Optional[datetime]
@@ -64,6 +66,6 @@ class ServiceFeedbackRead(ServiceFeedbackBase):
project: Optional[ProjectRead]
-class ServiceFeedbackPagination(DispatchBase):
+class ServiceFeedbackPagination(Pagination):
items: List[ServiceFeedbackRead]
total: int
diff --git a/src/dispatch/feedback/service/reminder/service.py b/src/dispatch/feedback/service/reminder/service.py
index ae264e1d403d..248f729e0f73 100644
--- a/src/dispatch/feedback/service/reminder/service.py
+++ b/src/dispatch/feedback/service/reminder/service.py
@@ -3,7 +3,6 @@
from .models import (
ServiceFeedbackReminder,
- ServiceFeedbackReminderCreate,
ServiceFeedbackReminderUpdate,
)
from dispatch.individual.models import IndividualContact
@@ -24,11 +23,11 @@ def get_all_expired_reminders_by_project_id(
)
-def create(*, db_session, reminder_in: ServiceFeedbackReminderCreate) -> ServiceFeedbackReminder:
+def create(*, db_session, reminder_in: ServiceFeedbackReminder) -> ServiceFeedbackReminder:
"""Creates a new service feedback reminder."""
reminder = ServiceFeedbackReminder(**reminder_in.dict())
- db_session.add(reminder_in)
+ db_session.add(reminder)
db_session.commit()
return reminder
@@ -55,5 +54,6 @@ def delete(*, db_session, reminder_id: int):
.filter(ServiceFeedbackReminder.id == reminder_id)
.one_or_none()
)
- db_session.delete(reminder)
- db_session.commit()
+ if reminder:
+ db_session.delete(reminder)
+ db_session.commit()
diff --git a/src/dispatch/feedback/service/scheduled.py b/src/dispatch/feedback/service/scheduled.py
index 6f5968659559..3c7a3c48f079 100644
--- a/src/dispatch/feedback/service/scheduled.py
+++ b/src/dispatch/feedback/service/scheduled.py
@@ -28,7 +28,7 @@
@timer
@scheduled_project_task
def oncall_shift_feedback_ucan(db_session: SessionLocal, project: Project):
- oncall_shift_feedback(db_session=db_session, project=project)
+ oncall_shift_feedback(db_session=db_session, project=project, hour=16)
find_expired_reminders_and_send(db_session=db_session, project=project)
@@ -36,7 +36,7 @@ def oncall_shift_feedback_ucan(db_session: SessionLocal, project: Project):
@timer
@scheduled_project_task
def oncall_shift_feedback_emea(db_session: SessionLocal, project: Project):
- oncall_shift_feedback(db_session=db_session, project=project)
+ oncall_shift_feedback(db_session=db_session, project=project, hour=6)
find_expired_reminders_and_send(db_session=db_session, project=project)
@@ -60,13 +60,18 @@ def find_expired_reminders_and_send(*, db_session: SessionLocal, project: Projec
def find_schedule_and_send(
- *, db_session: SessionLocal, project: Project, oncall_plugin: PluginInstance, schedule_id: str
+ *,
+ db_session: SessionLocal,
+ project: Project,
+ oncall_plugin: PluginInstance,
+ schedule_id: str,
+ hour: int,
):
"""
Given PagerDuty schedule_id, determine if the shift ended for the previous oncall person and
send the health metrics feedback request
"""
- current_oncall = oncall_plugin.instance.did_oncall_just_go_off_shift(schedule_id)
+ current_oncall = oncall_plugin.instance.did_oncall_just_go_off_shift(schedule_id, hour)
individual = individual_service.get_by_email_and_project(
db_session=db_session, email=current_oncall["email"], project_id=project.id
@@ -82,7 +87,7 @@ def find_schedule_and_send(
)
-def oncall_shift_feedback(db_session: SessionLocal, project: Project):
+def oncall_shift_feedback(db_session: SessionLocal, project: Project, hour: int):
"""
Experimental: collects feedback from individuals participating in an oncall service that has health metrics enabled
when their oncall shift ends. For now, only for one project and schedule.
@@ -115,4 +120,5 @@ def oncall_shift_feedback(db_session: SessionLocal, project: Project):
project=project,
oncall_plugin=oncall_plugin,
schedule_id=schedule_id,
+ hour=hour,
)
diff --git a/src/dispatch/feedback/service/views.py b/src/dispatch/feedback/service/views.py
new file mode 100644
index 000000000000..280ec390ac5e
--- /dev/null
+++ b/src/dispatch/feedback/service/views.py
@@ -0,0 +1,14 @@
+from fastapi import APIRouter
+
+from dispatch.database.service import search_filter_sort_paginate, CommonParameters
+
+from .models import ServiceFeedbackPagination
+
+
+router = APIRouter()
+
+
+@router.get("", response_model=ServiceFeedbackPagination)
+def get_feedback_entries(commons: CommonParameters):
+ """Get all feedback entries, or only those matching a given search term."""
+ return search_filter_sort_paginate(model="ServiceFeedback", **commons)
diff --git a/src/dispatch/incident/flows.py b/src/dispatch/incident/flows.py
index 7b397295661d..a174ff788a52 100644
--- a/src/dispatch/incident/flows.py
+++ b/src/dispatch/incident/flows.py
@@ -146,68 +146,74 @@ def inactivate_incident_participants(incident: Incident, db_session: Session):
)
-@background_task
-def incident_create_flow(*, organization_slug: str, incident_id: int, db_session=None) -> Incident:
- """Creates all resources required for new incidents."""
- # we get the incident
- incident = incident_service.get(db_session=db_session, incident_id=incident_id)
-
+def incident_create_resources(*, incident: Incident, db_session=None) -> Incident:
+ """Creates all resources required for incidents."""
# we create the incident ticket
- ticket_flows.create_incident_ticket(incident=incident, db_session=db_session)
+ if not incident.ticket:
+ ticket_flows.create_incident_ticket(incident=incident, db_session=db_session)
# we resolve individual and team participants
individual_participants, team_participants = get_incident_participants(incident, db_session)
+ tactical_participant_emails = [i.email for i, _ in individual_participants]
# we create the tactical group
- tactical_participant_emails = [i.email for i, _ in individual_participants]
- tactical_group = group_flows.create_group(
- subject=incident,
- group_type=GroupType.tactical,
- group_participants=tactical_participant_emails,
- db_session=db_session,
- )
+ if not incident.tactical_group:
+ group_flows.create_group(
+ subject=incident,
+ group_type=GroupType.tactical,
+ group_participants=tactical_participant_emails,
+ db_session=db_session,
+ )
# we create the notifications group
- notification_participant_emails = [t.email for t in team_participants]
- notifications_group = group_flows.create_group(
- subject=incident,
- group_type=GroupType.notifications,
- group_participants=notification_participant_emails,
- db_session=db_session,
- )
+ if not incident.notifications_group:
+ notification_participant_emails = [t.email for t in team_participants]
+ group_flows.create_group(
+ subject=incident,
+ group_type=GroupType.notifications,
+ group_participants=notification_participant_emails,
+ db_session=db_session,
+ )
# we create the storage folder
- storage_members = []
- if tactical_group and notifications_group:
- storage_members = [tactical_group.email, notifications_group.email]
- else:
- storage_members = tactical_participant_emails
+ if not incident.storage:
+ storage_members = []
+ if incident.tactical_group and incident.notifications_group:
+ storage_members = [incident.tactical_group.email, incident.notifications_group.email]
+ else:
+ storage_members = tactical_participant_emails
- storage_flows.create_storage(
- subject=incident, storage_members=storage_members, db_session=db_session
- )
+ storage_flows.create_storage(
+ subject=incident, storage_members=storage_members, db_session=db_session
+ )
# we create the incident document
- document_flows.create_document(
- subject=incident,
- document_type=DocumentResourceTypes.incident,
- document_template=incident.incident_type.incident_template_document,
- db_session=db_session,
- )
+ if not incident.incident_document:
+ document_flows.create_document(
+ subject=incident,
+ document_type=DocumentResourceTypes.incident,
+ document_template=incident.incident_type.incident_template_document,
+ db_session=db_session,
+ )
# we create the conference room
- conference_participants = []
- if tactical_group and notifications_group:
- conference_participants = [tactical_group.email, notifications_group.email]
- else:
- conference_participants = tactical_participant_emails
+ if not incident.conference:
+ conference_participants = []
+ if incident.tactical_group and incident.notifications_group:
+ conference_participants = [
+ incident.tactical_group.email,
+ incident.notifications_group.email,
+ ]
+ else:
+ conference_participants = tactical_participant_emails
- conference_flows.create_conference(
- incident=incident, participants=conference_participants, db_session=db_session
- )
+ conference_flows.create_conference(
+ incident=incident, participants=conference_participants, db_session=db_session
+ )
# we create the conversation
- conversation_flows.create_conversation(incident=incident, db_session=db_session)
+ if not incident.conversation:
+ conversation_flows.create_incident_conversation(incident=incident, db_session=db_session)
# we update the incident ticket
ticket_flows.update_incident_ticket(incident_id=incident.id, db_session=db_session)
@@ -244,7 +250,7 @@ def incident_create_flow(*, organization_slug: str, incident_id: int, db_session
)
# we add the participant to the conversation
- conversation_flows.add_participants(
+ conversation_flows.add_incident_participants(
incident=incident, participant_emails=[user_email], db_session=db_session
)
@@ -279,14 +285,31 @@ def incident_create_flow(*, organization_slug: str, incident_id: int, db_session
type=EventType.participant_updated,
)
- send_incident_created_notifications(incident, db_session)
+ return incident
- event_service.log_incident_event(
- db_session=db_session,
- source="Dispatch Core App",
- description="Incident notifications sent",
- incident_id=incident.id,
- )
+
+@background_task
+def incident_create_resources_flow(
+ *, organization_slug: str, incident_id: int, db_session=None
+) -> Incident:
+ """Creates all resources required for an existing incident."""
+ # we get the incident
+ incident = incident_service.get(db_session=db_session, incident_id=incident_id)
+
+ # we create the incident resources
+ return incident_create_resources(incident=incident, db_session=db_session)
+
+
+@background_task
+def incident_create_flow(*, organization_slug: str, incident_id: int, db_session=None) -> Incident:
+ """Creates all resources required for new incidents and initiates incident response workflow."""
+ # we get the incident
+ incident = incident_service.get(db_session=db_session, incident_id=incident_id)
+
+ # we create the incident resources
+ incident_create_resources(incident=incident, db_session=db_session)
+
+ send_incident_created_notifications(incident, db_session)
# we page the incident commander based on incident priority
if incident.incident_priority.page_commander:
@@ -917,7 +940,7 @@ def incident_add_or_reactivate_participant_flow(
if incident.status != IncidentStatus.closed:
# we add the participant to the conversation
- conversation_flows.add_participants(
+ conversation_flows.add_incident_participants(
incident=incident, participant_emails=[user_email], db_session=db_session
)
@@ -957,7 +980,7 @@ def incident_remove_participant_flow(
for assignee in task.assignees:
if assignee == participant:
# we add the participant to the conversation
- conversation_flows.add_participants(
+ conversation_flows.add_incident_participants(
incident=incident, participant_emails=[user_email], db_session=db_session
)
@@ -969,7 +992,7 @@ def incident_remove_participant_flow(
if user_email == incident.commander.individual.email:
# we add the participant to the conversation
- conversation_flows.add_participants(
+ conversation_flows.add_incident_participants(
incident=incident, participant_emails=[user_email], db_session=db_session
)
diff --git a/src/dispatch/incident/messaging.py b/src/dispatch/incident/messaging.py
index 1c6f1d4b2575..994ae45d4564 100644
--- a/src/dispatch/incident/messaging.py
+++ b/src/dispatch/incident/messaging.py
@@ -11,6 +11,7 @@
from dispatch.conversation.enums import ConversationCommands
from dispatch.database.core import SessionLocal, resolve_attr
from dispatch.document import service as document_service
+from dispatch.event import service as event_service
from dispatch.incident.enums import IncidentStatus
from dispatch.incident.models import Incident, IncidentRead
from dispatch.notification import service as notification_service
@@ -347,6 +348,13 @@ def send_incident_created_notifications(incident: Incident, db_session: SessionL
notification_params=notification_params,
)
+ event_service.log_incident_event(
+ db_session=db_session,
+ source="Dispatch Core App",
+ description="Incident notifications sent",
+ incident_id=incident.id,
+ )
+
log.debug("Incident created notifications sent.")
diff --git a/src/dispatch/incident/views.py b/src/dispatch/incident/views.py
index dacfa5f04b7c..1c95931a113a 100644
--- a/src/dispatch/incident/views.py
+++ b/src/dispatch/incident/views.py
@@ -39,6 +39,7 @@
incident_delete_flow,
incident_subscribe_participant_flow,
incident_update_flow,
+ incident_create_resources_flow,
)
from .metrics import create_incident_metric_query, make_forecast
from .models import (
@@ -142,6 +143,25 @@ def create_incident(
return incident
+@router.post(
+ "/{incident_id}/resources",
+ response_model=IncidentRead,
+ summary="Creates resources for an existing incident.",
+)
+def create_incident_resources(
+ organization: OrganizationSlug,
+ incident_id: PrimaryKey,
+ current_incident: CurrentIncident,
+ background_tasks: BackgroundTasks,
+):
+ """Creates resources for an existing incident."""
+ background_tasks.add_task(
+ incident_create_resources_flow, organization_slug=organization, incident_id=incident_id
+ )
+
+ return current_incident
+
+
@router.put(
"/{incident_id}",
response_model=IncidentRead,
diff --git a/src/dispatch/plugins/dispatch_pagerduty/plugin.py b/src/dispatch/plugins/dispatch_pagerduty/plugin.py
index 1e7303d8bb32..b30b0456bdd2 100644
--- a/src/dispatch/plugins/dispatch_pagerduty/plugin.py
+++ b/src/dispatch/plugins/dispatch_pagerduty/plugin.py
@@ -83,12 +83,13 @@ def page(
incident_description=incident_description,
)
- def did_oncall_just_go_off_shift(self, schedule_id: str) -> Optional[dict]:
+ def did_oncall_just_go_off_shift(self, schedule_id: str, hour: int) -> Optional[dict]:
client = APISession(self.configuration.api_key.get_secret_value())
client.url = self.configuration.pagerduty_api_url
return oncall_shift_check(
client=client,
schedule_id=schedule_id,
+ hour=hour,
)
def get_schedule_id_from_service_id(self, service_id: str) -> Optional[str]:
diff --git a/src/dispatch/plugins/dispatch_pagerduty/service.py b/src/dispatch/plugins/dispatch_pagerduty/service.py
index 130401e841e2..89079192ff40 100644
--- a/src/dispatch/plugins/dispatch_pagerduty/service.py
+++ b/src/dispatch/plugins/dispatch_pagerduty/service.py
@@ -177,9 +177,12 @@ def get_oncall_at_time(client: APISession, schedule_id: str, utctime: str) -> Op
raise e
-def oncall_shift_check(client: APISession, schedule_id: str) -> Optional[dict]:
+def oncall_shift_check(client: APISession, schedule_id: str, hour: int) -> Optional[dict]:
"""Determines whether the oncall person just went off shift and returns their email."""
now = datetime.utcnow()
+ # in case scheduler is late, replace hour with exact one for shift comparison
+ now = now.replace(hour=hour, minute=0, second=0, microsecond=0)
+
# compare oncall person scheduled 18 hours ago vs 2 hours from now
previous_shift = (now - timedelta(hours=18)).isoformat(timespec="minutes") + "Z"
next_shift = (now + timedelta(hours=2)).isoformat(timespec="minutes") + "Z"
diff --git a/src/dispatch/plugins/dispatch_slack/case/interactive.py b/src/dispatch/plugins/dispatch_slack/case/interactive.py
index e4797c36d368..a49ad567497b 100644
--- a/src/dispatch/plugins/dispatch_slack/case/interactive.py
+++ b/src/dispatch/plugins/dispatch_slack/case/interactive.py
@@ -1032,7 +1032,7 @@ def handle_escalation_submission_event(
case=case, organization_slug=context["subject"].organization_slug, db_session=db_session
)
- conversation_flows.add_participants(
+ conversation_flows.add_incident_participants(
incident=incident, participant_emails=[user.email], db_session=db_session
)
@@ -1082,7 +1082,7 @@ def join_incident_button_click(
case = case_service.get(db_session=db_session, case_id=context["subject"].id)
# we add the user to the incident conversation
- conversation_flows.add_participants(
+ conversation_flows.add_incident_participants(
# TODO: handle case where there are multiple related incidents
incident=case.incidents[0],
participant_emails=[user.email],
diff --git a/src/dispatch/plugins/dispatch_slack/feedback/interactive.py b/src/dispatch/plugins/dispatch_slack/feedback/interactive.py
index 307b837fa18b..ea61d7665323 100644
--- a/src/dispatch/plugins/dispatch_slack/feedback/interactive.py
+++ b/src/dispatch/plugins/dispatch_slack/feedback/interactive.py
@@ -186,7 +186,7 @@ def handle_incident_feedback_submission_event(
ack_incident_feedback_submission_event(ack=ack)
incident = incident_service.get(db_session=db_session, incident_id=context["subject"].id)
- feedback = form_data.get(IncidentFeedbackNotificationBlockIds.feedback_input)
+ feedback = form_data.get(IncidentFeedbackNotificationBlockIds.feedback_input, "")
rating = form_data.get(IncidentFeedbackNotificationBlockIds.rating_select, {}).get("value")
feedback_in = FeedbackCreate(
@@ -275,8 +275,8 @@ def oncall_shift_feedback_input(
initial_value=initial_value,
multiline=True,
placeholder="How would you describe your experience?",
- optional=True,
),
+ optional=True,
label=label,
**kwargs,
)
@@ -448,7 +448,7 @@ def handle_oncall_shift_feedback_submission_event(
]
try:
plugin.instance.send_direct(
- individual.email,
+ user.email,
notification_text,
notification_template,
MessageType.service_feedback,
diff --git a/src/dispatch/static/dispatch/package-lock.json b/src/dispatch/static/dispatch/package-lock.json
index 1be988761dd3..85d77f4e0cc1 100644
--- a/src/dispatch/static/dispatch/package-lock.json
+++ b/src/dispatch/static/dispatch/package-lock.json
@@ -584,9 +584,9 @@
"dev": true
},
"node_modules/@eslint/js": {
- "version": "8.49.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz",
- "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==",
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz",
+ "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -712,9 +712,9 @@
}
},
"node_modules/@koumoul/vjsf": {
- "version": "2.22.0",
- "resolved": "https://registry.npmjs.org/@koumoul/vjsf/-/vjsf-2.22.0.tgz",
- "integrity": "sha512-qQ23G+OX8Vf16K3ysxb7axk5A+tREjVwCxBShvvlQ9NETNqifpBqhHcjwMtkS0kxf7f8yA+hMWnJczm57d+EYA==",
+ "version": "2.22.1",
+ "resolved": "https://registry.npmjs.org/@koumoul/vjsf/-/vjsf-2.22.1.tgz",
+ "integrity": "sha512-u5yaNTzeCTNyhJlhM6zLGbf7EPQQgBR64kNF1b3EMZFcXTpmClewU1n+lg8Wj6f5RDPLwAwlVSNPMdgpfMD9jg==",
"dependencies": {
"debounce": "^1.2.1",
"debounce-promise": "^3.1.2",
@@ -816,12 +816,12 @@
}
},
"node_modules/@playwright/test": {
- "version": "1.38.0",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.0.tgz",
- "integrity": "sha512-xis/RXXsLxwThKnlIXouxmIvvT3zvQj1JE39GsNieMUrMpb3/GySHDh2j8itCG22qKVD4MYLBp7xB73cUW/UUw==",
+ "version": "1.38.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz",
+ "integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==",
"dev": true,
"dependencies": {
- "playwright": "1.38.0"
+ "playwright": "1.38.1"
},
"bin": {
"playwright": "cli.js"
@@ -2595,15 +2595,15 @@
}
},
"node_modules/eslint": {
- "version": "8.49.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz",
- "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==",
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz",
+ "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.2",
- "@eslint/js": "8.49.0",
+ "@eslint/js": "8.50.0",
"@humanwhocodes/config-array": "^0.11.11",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
@@ -4820,12 +4820,12 @@
}
},
"node_modules/playwright": {
- "version": "1.38.0",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.0.tgz",
- "integrity": "sha512-fJGw+HO0YY+fU/F1N57DMO+TmXHTrmr905J05zwAQE9xkuwP/QLDk63rVhmyxh03dYnEhnRbsdbH9B0UVVRB3A==",
+ "version": "1.38.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz",
+ "integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==",
"dev": true,
"dependencies": {
- "playwright-core": "1.38.0"
+ "playwright-core": "1.38.1"
},
"bin": {
"playwright": "cli.js"
@@ -4838,9 +4838,9 @@
}
},
"node_modules/playwright-core": {
- "version": "1.38.0",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.0.tgz",
- "integrity": "sha512-f8z1y8J9zvmHoEhKgspmCvOExF2XdcxMW8jNRuX4vkQFrzV4MlZ55iwb5QeyiFQgOFCUolXiRHgpjSEnqvO48g==",
+ "version": "1.38.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz",
+ "integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
@@ -7053,9 +7053,9 @@
}
},
"@eslint/js": {
- "version": "8.49.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz",
- "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==",
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz",
+ "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==",
"dev": true
},
"@humanwhocodes/config-array": {
@@ -7159,9 +7159,9 @@
}
},
"@koumoul/vjsf": {
- "version": "2.22.0",
- "resolved": "https://registry.npmjs.org/@koumoul/vjsf/-/vjsf-2.22.0.tgz",
- "integrity": "sha512-qQ23G+OX8Vf16K3ysxb7axk5A+tREjVwCxBShvvlQ9NETNqifpBqhHcjwMtkS0kxf7f8yA+hMWnJczm57d+EYA==",
+ "version": "2.22.1",
+ "resolved": "https://registry.npmjs.org/@koumoul/vjsf/-/vjsf-2.22.1.tgz",
+ "integrity": "sha512-u5yaNTzeCTNyhJlhM6zLGbf7EPQQgBR64kNF1b3EMZFcXTpmClewU1n+lg8Wj6f5RDPLwAwlVSNPMdgpfMD9jg==",
"requires": {
"@mdi/font": "^6.5.95",
"@mdi/js": "^6.5.95",
@@ -7248,12 +7248,12 @@
}
},
"@playwright/test": {
- "version": "1.38.0",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.0.tgz",
- "integrity": "sha512-xis/RXXsLxwThKnlIXouxmIvvT3zvQj1JE39GsNieMUrMpb3/GySHDh2j8itCG22qKVD4MYLBp7xB73cUW/UUw==",
+ "version": "1.38.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz",
+ "integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==",
"dev": true,
"requires": {
- "playwright": "1.38.0"
+ "playwright": "1.38.1"
}
},
"@rollup/pluginutils": {
@@ -8540,15 +8540,15 @@
}
},
"eslint": {
- "version": "8.49.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz",
- "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==",
+ "version": "8.50.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz",
+ "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.2",
- "@eslint/js": "8.49.0",
+ "@eslint/js": "8.50.0",
"@humanwhocodes/config-array": "^0.11.11",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
@@ -10300,19 +10300,19 @@
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
},
"playwright": {
- "version": "1.38.0",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.0.tgz",
- "integrity": "sha512-fJGw+HO0YY+fU/F1N57DMO+TmXHTrmr905J05zwAQE9xkuwP/QLDk63rVhmyxh03dYnEhnRbsdbH9B0UVVRB3A==",
+ "version": "1.38.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz",
+ "integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==",
"dev": true,
"requires": {
"fsevents": "2.3.2",
- "playwright-core": "1.38.0"
+ "playwright-core": "1.38.1"
}
},
"playwright-core": {
- "version": "1.38.0",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.0.tgz",
- "integrity": "sha512-f8z1y8J9zvmHoEhKgspmCvOExF2XdcxMW8jNRuX4vkQFrzV4MlZ55iwb5QeyiFQgOFCUolXiRHgpjSEnqvO48g==",
+ "version": "1.38.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz",
+ "integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==",
"dev": true
},
"postcss": {
diff --git a/src/dispatch/static/dispatch/src/case/ResourcesTab.vue b/src/dispatch/static/dispatch/src/case/ResourcesTab.vue
index ad7403449c82..a9eb2b5d5df4 100644
--- a/src/dispatch/static/dispatch/src/case/ResourcesTab.vue
+++ b/src/dispatch/static/dispatch/src/case/ResourcesTab.vue
@@ -65,23 +65,83 @@
+
+
+
+ Recreate Missing Resources
+ Initiate a retry for creating any missing or unsuccesfully created
+ resource(s).
+
+
+
+ refresh
+
+
+
+
+
+ Creating resources...
+ Initiate a retry for creating any missing or unsuccesfully created
+ resource(s).
+
+
+
diff --git a/src/dispatch/static/dispatch/src/case/api.js b/src/dispatch/static/dispatch/src/case/api.js
index 8a3b0d2abb61..2fc1a01f27e3 100644
--- a/src/dispatch/static/dispatch/src/case/api.js
+++ b/src/dispatch/static/dispatch/src/case/api.js
@@ -44,4 +44,8 @@ export default {
delete(caseId) {
return API.delete(`/${resource}/${caseId}`)
},
+
+ createAllResources(caseId, payload) {
+ return API.post(`/${resource}/${caseId}/resources`, payload)
+ },
}
diff --git a/src/dispatch/static/dispatch/src/case/store.js b/src/dispatch/static/dispatch/src/case/store.js
index bbe3ec901dac..6eda8cc9b416 100644
--- a/src/dispatch/static/dispatch/src/case/store.js
+++ b/src/dispatch/static/dispatch/src/case/store.js
@@ -3,6 +3,7 @@ import { debounce } from "lodash"
import SearchUtils from "@/search/utils"
import CaseApi from "@/case/api"
+import PluginApi from "@/plugin/api"
import router from "@/router"
const getDefaultSelectedState = () => {
@@ -253,6 +254,40 @@ const actions = {
commit("SET_SELECTED_LOADING", false)
})
},
+ createAllResources({ commit, dispatch }) {
+ commit("SET_SELECTED_LOADING", true)
+ return CaseApi.createAllResources(state.selected.id)
+ .then(() => {
+ CaseApi.get(state.selected.id).then((response) => {
+ commit("SET_SELECTED", response.data)
+ dispatch("getEnabledPlugins").then((enabledPlugins) => {
+ // Poll the server for resource creation updates.
+ var interval = setInterval(function () {
+ if (
+ state.selected.conversation ^ enabledPlugins.includes("conversation") ||
+ state.selected.documents ^ enabledPlugins.includes("document") ||
+ state.selected.storage ^ enabledPlugins.includes("storage") ||
+ state.selected.groups ^ enabledPlugins.includes("participant-group") ||
+ state.selected.ticket ^ enabledPlugins.includes("ticket")
+ ) {
+ dispatch("get").then(() => {
+ clearInterval(interval)
+ commit("SET_SELECTED_LOADING", false)
+ commit(
+ "notification_backend/addBeNotification",
+ { text: "Resources(s) created successfully.", type: "success" },
+ { root: true }
+ )
+ })
+ }
+ }, 5000)
+ })
+ })
+ })
+ .catch(() => {
+ commit("SET_SELECTED_LOADING", false)
+ })
+ },
save({ commit, dispatch }) {
commit("SET_SELECTED_LOADING", true)
if (!state.selected.id) {
@@ -330,6 +365,37 @@ const actions = {
)
})
},
+ getEnabledPlugins() {
+ if (!state.selected.project) {
+ return false
+ }
+ return PluginApi.getAllInstances({
+ filter: JSON.stringify({
+ and: [
+ {
+ model: "PluginInstance",
+ field: "enabled",
+ op: "==",
+ value: "true",
+ },
+ {
+ model: "Project",
+ field: "name",
+ op: "==",
+ value: state.selected.project.name,
+ },
+ ],
+ }),
+ itemsPerPage: 50,
+ }).then((response) => {
+ return response.data.items.reduce((result, item) => {
+ if (item.plugin) {
+ result.push(item.plugin.type)
+ }
+ return result
+ }, [])
+ })
+ },
}
const mutations = {
diff --git a/src/dispatch/static/dispatch/src/dashboard/incident/IncidentDialogFilter.vue b/src/dispatch/static/dispatch/src/dashboard/incident/IncidentDialogFilter.vue
index 4c9bc4394fd8..28532489bf0a 100644
--- a/src/dispatch/static/dispatch/src/dashboard/incident/IncidentDialogFilter.vue
+++ b/src/dispatch/static/dispatch/src/dashboard/incident/IncidentDialogFilter.vue
@@ -45,6 +45,28 @@
+
+
+
+ Incident Participant
+ Show only incidents with this participant
+
+
+
+
+
@@ -70,6 +92,7 @@ import ProjectCombobox from "@/project/ProjectCombobox.vue"
import RouterUtils from "@/router/utils"
import SearchUtils from "@/search/utils"
import TagFilterAutoComplete from "@/tag/TagFilterAutoComplete.vue"
+import ParticipantSelect from "@/incident/ParticipantSelect.vue"
let today = function () {
let now = new Date()
@@ -86,6 +109,7 @@ export default {
IncidentTypeCombobox,
ProjectCombobox,
TagFilterAutoComplete,
+ ParticipantSelect,
},
props: {
@@ -118,6 +142,8 @@ export default {
end: null,
},
},
+ local_participant_is_commander: false,
+ local_participant: null,
}
},
@@ -130,6 +156,7 @@ export default {
this.filters.project.length,
this.filters.status.length,
this.filters.tag.length,
+ this.local_participant == null ? 0 : 1,
1,
])
},
@@ -138,6 +165,15 @@ export default {
methods: {
applyFilters() {
+ if (this.local_participant) {
+ if (this.local_participant_is_commander) {
+ this.filters.commander = this.local_participant
+ this.filters.participant = null
+ } else {
+ this.filters.commander = null
+ this.filters.participant = this.local_participant
+ }
+ }
RouterUtils.updateURLFilters(this.filters)
this.fetchData()
// we close the dialog
diff --git a/src/dispatch/static/dispatch/src/incident/ResourcesTab.vue b/src/dispatch/static/dispatch/src/incident/ResourcesTab.vue
index 9c1a3d2c9f15..b478fb9ecb6e 100644
--- a/src/dispatch/static/dispatch/src/incident/ResourcesTab.vue
+++ b/src/dispatch/static/dispatch/src/incident/ResourcesTab.vue
@@ -62,23 +62,83 @@
+
+
+
+ Recreate Missing Resources
+ Initiate a retry for creating any missing or unsuccesfully created
+ resource(s).
+
+
+
+ refresh
+
+
+
+
+
+ Creating resources...
+ Initiate a retry for creating any missing or unsuccesfully created
+ resource(s).
+
+
+
diff --git a/src/dispatch/static/dispatch/src/incident/api.js b/src/dispatch/static/dispatch/src/incident/api.js
index 2df49e9908b5..26debe2a1895 100644
--- a/src/dispatch/static/dispatch/src/incident/api.js
+++ b/src/dispatch/static/dispatch/src/incident/api.js
@@ -70,4 +70,8 @@ export default {
deleteEvent(incidentId, event_uuid) {
return API.delete(`/${resource}/${incidentId}/event/${event_uuid}`)
},
+
+ createAllResources(incidentId, payload) {
+ return API.post(`/${resource}/${incidentId}/resources`, payload)
+ },
}
diff --git a/src/dispatch/static/dispatch/src/incident/store.js b/src/dispatch/static/dispatch/src/incident/store.js
index ef4980f4e734..34287eea32b9 100644
--- a/src/dispatch/static/dispatch/src/incident/store.js
+++ b/src/dispatch/static/dispatch/src/incident/store.js
@@ -3,6 +3,7 @@ import { debounce } from "lodash"
import SearchUtils from "@/search/utils"
import IncidentApi from "@/incident/api"
+import PluginApi from "@/plugin/api"
import router from "@/router"
import moment from "moment-timezone"
@@ -428,6 +429,40 @@ const actions = {
)
})
},
+ createAllResources({ commit, dispatch }) {
+ commit("SET_SELECTED_LOADING", true)
+ return IncidentApi.createAllResources(state.selected.id)
+ .then(() => {
+ IncidentApi.get(state.selected.id).then((response) => {
+ commit("SET_SELECTED", response.data)
+ dispatch("getEnabledPlugins").then((enabledPlugins) => {
+ // Poll the server for resource creation updates.
+ var interval = setInterval(function () {
+ if (
+ state.selected.conversation ^ enabledPlugins.includes("conversation") ||
+ state.selected.documents ^ enabledPlugins.includes("document") ||
+ state.selected.storage ^ enabledPlugins.includes("storage") ||
+ state.selected.conference ^ enabledPlugins.includes("conference") ||
+ state.selected.ticket ^ enabledPlugins.includes("ticket")
+ ) {
+ dispatch("get").then(() => {
+ clearInterval(interval)
+ commit("SET_SELECTED_LOADING", false)
+ commit(
+ "notification_backend/addBeNotification",
+ { text: "Resources(s) created successfully.", type: "success" },
+ { root: true }
+ )
+ })
+ }
+ }, 5000)
+ })
+ })
+ })
+ .catch(() => {
+ commit("SET_SELECTED_LOADING", false)
+ })
+ },
resetSelected({ commit }) {
commit("RESET_SELECTED")
},
@@ -452,6 +487,37 @@ const actions = {
)
})
},
+ getEnabledPlugins() {
+ if (!state.selected.project) {
+ return false
+ }
+ return PluginApi.getAllInstances({
+ filter: JSON.stringify({
+ and: [
+ {
+ model: "PluginInstance",
+ field: "enabled",
+ op: "==",
+ value: "true",
+ },
+ {
+ model: "Project",
+ field: "name",
+ op: "==",
+ value: state.selected.project.name,
+ },
+ ],
+ }),
+ itemsPerPage: 50,
+ }).then((response) => {
+ return response.data.items.reduce((result, item) => {
+ if (item.plugin) {
+ result.push(item.plugin.type)
+ }
+ return result
+ }, [])
+ })
+ },
}
const mutations = {
diff --git a/src/dispatch/static/dispatch/src/router/utils.js b/src/dispatch/static/dispatch/src/router/utils.js
index 081d8b8f64e1..ccd5c72c20ef 100644
--- a/src/dispatch/static/dispatch/src/router/utils.js
+++ b/src/dispatch/static/dispatch/src/router/utils.js
@@ -14,17 +14,33 @@ export default {
return
}
each(value, function (item) {
- if (has(flatFilters, key)) {
- if (typeof item === "string" || item instanceof String) {
- flatFilters[key].push(item)
+ if (["commander", "participant"].includes(key)) {
+ if (has(flatFilters, key)) {
+ if (typeof item === "string" || item instanceof String) {
+ flatFilters[key].push(item)
+ } else {
+ flatFilters[key].push(item.email)
+ }
} else {
- flatFilters[key].push(item.name)
+ if (typeof item === "string" || item instanceof String) {
+ flatFilters[key] = [item]
+ } else {
+ flatFilters[key] = [item.email]
+ }
}
} else {
- if (typeof item === "string" || item instanceof String) {
- flatFilters[key] = [item]
+ if (has(flatFilters, key)) {
+ if (typeof item === "string" || item instanceof String) {
+ flatFilters[key].push(item)
+ } else {
+ flatFilters[key].push(item.name)
+ }
} else {
- flatFilters[key] = [item.name]
+ if (typeof item === "string" || item instanceof String) {
+ flatFilters[key] = [item]
+ } else {
+ flatFilters[key] = [item.name]
+ }
}
}
})
@@ -69,6 +85,24 @@ export default {
}
return
}
+ if (["commander", "participant"].includes(key)) {
+ if (typeof value === "string" || value instanceof String) {
+ if (has(filters, key)) {
+ filters[key].push({ email: value })
+ } else {
+ filters[key] = [{ email: value }]
+ }
+ } else {
+ each(value, function (item) {
+ if (has(filters, key)) {
+ filters[key].push({ email: item })
+ } else {
+ filters[key] = [{ email: item }]
+ }
+ })
+ }
+ return
+ }
if (typeof value === "string" || value instanceof String) {
if (has(filters, key)) {
filters[key].push({ name: value })
diff --git a/src/dispatch/static/dispatch/src/search/utils.js b/src/dispatch/static/dispatch/src/search/utils.js
index 3d1be804615f..20438a2c5183 100644
--- a/src/dispatch/static/dispatch/src/search/utils.js
+++ b/src/dispatch/static/dispatch/src/search/utils.js
@@ -118,7 +118,14 @@ export default {
if (!value) {
return
}
- if (has(value, "id")) {
+ if (["commander", "participant"].includes(key) && has(value, "email")) {
+ subFilter.push({
+ model: toPascalCase(key),
+ field: "email",
+ op: "==",
+ value: value.email,
+ })
+ } else if (has(value, "id")) {
subFilter.push({
model: toPascalCase(key),
field: "id",
diff --git a/src/dispatch/static/dispatch/src/service_feedback/api.js b/src/dispatch/static/dispatch/src/service_feedback/api.js
new file mode 100644
index 000000000000..95b15f247874
--- /dev/null
+++ b/src/dispatch/static/dispatch/src/service_feedback/api.js
@@ -0,0 +1,9 @@
+import API from "@/api"
+
+const resource = "/service_feedback"
+
+export default {
+ getAll(options) {
+ return API.get(`${resource}`, { params: { ...options } })
+ },
+}