From 24b78e500df92c21546f45309e882ca497ea23ba Mon Sep 17 00:00:00 2001
From: David Whittaker <84562015+whitdog47@users.noreply.github.com>
Date: Thu, 24 Oct 2024 09:31:53 -0700
Subject: [PATCH 1/6] fix(ui): fixes details tab selects (#5385)
---
.github/workflows/python.yml | 2 +-
.../static/dispatch/src/case/DetailsTab.vue | 2 +-
.../src/case/priority/CasePrioritySelect.vue | 18 +-----
.../dispatch/src/case/type/CaseTypeSelect.vue | 21 ++-----
.../dispatch/src/incident/DetailsTab.vue | 2 +-
.../priority/IncidentPrioritySelect.vue | 20 +------
.../src/incident/type/IncidentTypeSelect.vue | 56 +++++++++++--------
7 files changed, 44 insertions(+), 77 deletions(-)
diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml
index f45cf86f1c08..4bbb95877b30 100644
--- a/.github/workflows/python.yml
+++ b/.github/workflows/python.yml
@@ -8,7 +8,7 @@ jobs:
# Minimum code coverage per file
COVERAGE_SINGLE: 50
# Minimum total code coverage
- COVERAGE_TOTAL: 56
+ COVERAGE_TOTAL: 55
runs-on: ubuntu-latest
services:
postgres:
diff --git a/src/dispatch/static/dispatch/src/case/DetailsTab.vue b/src/dispatch/static/dispatch/src/case/DetailsTab.vue
index b216d2d19186..6873d90c9038 100644
--- a/src/dispatch/static/dispatch/src/case/DetailsTab.vue
+++ b/src/dispatch/static/dispatch/src/case/DetailsTab.vue
@@ -62,7 +62,7 @@
/>
-
+
diff --git a/src/dispatch/static/dispatch/src/case/priority/CasePrioritySelect.vue b/src/dispatch/static/dispatch/src/case/priority/CasePrioritySelect.vue
index ecbc78c6b2dd..47fb41115fe8 100644
--- a/src/dispatch/static/dispatch/src/case/priority/CasePrioritySelect.vue
+++ b/src/dispatch/static/dispatch/src/case/priority/CasePrioritySelect.vue
@@ -140,22 +140,8 @@ export default {
},
watch: {
- project: {
- handler(newProject) {
- if (newProject?.id !== this.lastProjectId) {
- // Check if we're moving to a valid project (not null)
- if (this.lastProjectId) {
- this.lastProjectId = newProject.id
- this.resetSelection()
- this.fetchData()
- } else {
- // If new project is null/undefined, just update lastProjectId
- this.lastProjectId = null
- }
- }
- this.validatePriority()
- },
- deep: true,
+ project() {
+ this.fetchData()
},
},
diff --git a/src/dispatch/static/dispatch/src/case/type/CaseTypeSelect.vue b/src/dispatch/static/dispatch/src/case/type/CaseTypeSelect.vue
index ce9693799055..dcf695fa1f6d 100644
--- a/src/dispatch/static/dispatch/src/case/type/CaseTypeSelect.vue
+++ b/src/dispatch/static/dispatch/src/case/type/CaseTypeSelect.vue
@@ -130,7 +130,7 @@ export default {
}
}
- filterOptions = SearchUtils.createParametersFromTableOptions({ ...filterOptions })
+ filterOptions = SearchUtils.createParametersFromTableOptions({ ...filterOptions }, "CaseType")
CaseTypeApi.getAll(filterOptions)
.then((response) => {
@@ -169,22 +169,9 @@ export default {
},
watch: {
- project: {
- handler(newProject) {
- if (newProject?.id !== this.lastProjectId) {
- // Check if we're moving to a valid project (not null)
- if (this.lastProjectId) {
- this.lastProjectId = newProject.id
- this.resetSelection()
- this.fetchData()
- } else {
- // If new project is null/undefined, just update lastProjectId
- this.lastProjectId = null
- }
- }
- this.validateType()
- },
- deep: true,
+ project() {
+ this.validateType()
+ this.fetchData()
},
},
diff --git a/src/dispatch/static/dispatch/src/incident/DetailsTab.vue b/src/dispatch/static/dispatch/src/incident/DetailsTab.vue
index 945b18d96c0e..ac05614ad7e7 100644
--- a/src/dispatch/static/dispatch/src/incident/DetailsTab.vue
+++ b/src/dispatch/static/dispatch/src/incident/DetailsTab.vue
@@ -56,7 +56,7 @@
/>
-
+
diff --git a/src/dispatch/static/dispatch/src/incident/priority/IncidentPrioritySelect.vue b/src/dispatch/static/dispatch/src/incident/priority/IncidentPrioritySelect.vue
index 9feb7e5e4353..a14387ae925a 100644
--- a/src/dispatch/static/dispatch/src/incident/priority/IncidentPrioritySelect.vue
+++ b/src/dispatch/static/dispatch/src/incident/priority/IncidentPrioritySelect.vue
@@ -130,23 +130,9 @@ export default {
},
watch: {
- project: {
- handler(newProject) {
- if (newProject?.id !== this.lastProjectId) {
- // Check if we're moving to a valid project (not null)
- if (this.lastProjectId) {
- this.lastProjectId = newProject.id
- this.resetSelection()
- this.fetchData()
- } else {
- // If new project is null/undefined, just update lastProjectId
- this.lastProjectId = null
- }
- }
-
- this.validatePriority()
- },
- deep: true,
+ project() {
+ this.validatePriority()
+ this.fetchData()
},
status() {
this.validatePriority()
diff --git a/src/dispatch/static/dispatch/src/incident/type/IncidentTypeSelect.vue b/src/dispatch/static/dispatch/src/incident/type/IncidentTypeSelect.vue
index 5040af7002bb..e51c9fa58a4a 100644
--- a/src/dispatch/static/dispatch/src/incident/type/IncidentTypeSelect.vue
+++ b/src/dispatch/static/dispatch/src/incident/type/IncidentTypeSelect.vue
@@ -7,7 +7,7 @@
:label="label"
return-object
:loading="loading"
- :rules="[validationRule]"
+ :rules="[is_type_in_project]"
>
@@ -61,46 +61,51 @@ export default {
numItems: 5,
total: 0,
lastProjectId: null,
+ error: null,
+ is_type_in_project: () => {
+ this.validateType()
+ return this.error
+ },
}
},
computed: {
selectedIncidentType: {
get() {
- return this.modelValue || null
+ if (!this.modelValue) return null
+ if (this.modelValue.id) {
+ return this.items.find((item) => item.id === this.modelValue.id) || null
+ }
+ // If we only have a name (e.g., from URL params), find by name
+ if (this.modelValue.name) {
+ return this.items.find((item) => item.name === this.modelValue.name) || null
+ }
+ return null
},
set(value) {
this.$emit("update:modelValue", value)
+ this.validateType()
},
},
- isTypeValid() {
- const project_id = this.project?.id || 0
- return this.selectedIncidentType?.project?.id == project_id
- },
- validationRule() {
- return this.isTypeValid || "Only types in selected project are allowed"
- },
},
watch: {
- project: {
- handler(newProject) {
- if (newProject?.id !== this.lastProjectId) {
- // Check if we're moving to a valid project (not null)
- if (this.lastProjectId) {
- this.lastProjectId = newProject.id
- this.resetSelection()
- this.fetchData()
- } else {
- // If new project is null/undefined, just update lastProjectId
- this.lastProjectId = null
- }
- }
- },
+ project() {
+ this.validateType()
+ this.fetchData()
},
},
methods: {
+ validateType() {
+ const project_id = this.project?.id || 0
+ const in_project = this.selectedIncidentType?.project?.id == project_id
+ if (in_project) {
+ this.error = true
+ } else {
+ this.error = "Only types in selected project are allowed"
+ }
+ },
clearSelection() {
this.selectedIncidentType = null
},
@@ -124,7 +129,10 @@ export default {
}
}
- filterOptions = SearchUtils.createParametersFromTableOptions(filterOptions)
+ filterOptions = SearchUtils.createParametersFromTableOptions(
+ { ...filterOptions },
+ "IncidentType"
+ )
IncidentTypeApi.getAll(filterOptions)
.then((response) => {
From 6b01581a85f2408eb779c29d9a48fb52ce535745 Mon Sep 17 00:00:00 2001
From: David Whittaker <84562015+whitdog47@users.noreply.github.com>
Date: Thu, 24 Oct 2024 17:06:46 -0700
Subject: [PATCH 2/6] increasing type selector length (#5389)
---
.../src/incident/type/IncidentTypeSelect.vue | 2 +-
tests/static/e2e/report-submission.spec.ts | 28 +++++++++----------
2 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/src/dispatch/static/dispatch/src/incident/type/IncidentTypeSelect.vue b/src/dispatch/static/dispatch/src/incident/type/IncidentTypeSelect.vue
index e51c9fa58a4a..0182b9d0d27c 100644
--- a/src/dispatch/static/dispatch/src/incident/type/IncidentTypeSelect.vue
+++ b/src/dispatch/static/dispatch/src/incident/type/IncidentTypeSelect.vue
@@ -58,7 +58,7 @@ export default {
loading: false,
items: [],
more: false,
- numItems: 5,
+ numItems: 50,
total: 0,
lastProjectId: null,
error: null,
diff --git a/tests/static/e2e/report-submission.spec.ts b/tests/static/e2e/report-submission.spec.ts
index 839547172a88..1f7fe420871a 100644
--- a/tests/static/e2e/report-submission.spec.ts
+++ b/tests/static/e2e/report-submission.spec.ts
@@ -43,19 +43,19 @@ test.describe("Authenticated Dispatch App", () => {
"'Incident Report: Description' not visible on page after submission of incident report."
)
.toBeVisible()
- }),
- test("The 'Load More' selector should be visible when there are more than 5 options in Type combobox.", async ({
- reportIncidentPage,
- }) => {
- await reportIncidentPage.goto()
- await reportIncidentPage.typeDropdown.click()
-
- // Soft check that the 'Load More' selector is available upon opening the Project dropdown
- await expect
- .soft(
- reportIncidentPage.loadMore,
- "The 'Load More' selector should be visible when there are more than 5 options in Type combobox."
- )
- .toBeVisible()
})
+ // test("The 'Load More' selector should be visible when there are more than 5 options in Type combobox.", async ({
+ // reportIncidentPage,
+ // }) => {
+ // await reportIncidentPage.goto()
+ // await reportIncidentPage.typeDropdown.click()
+
+ // // Soft check that the 'Load More' selector is available upon opening the Project dropdown
+ // await expect
+ // .soft(
+ // reportIncidentPage.loadMore,
+ // "The 'Load More' selector should be visible when there are more than 5 options in Type combobox."
+ // )
+ // .toBeVisible()
+ // })
})
From f2a14934bf77c6354e4e1229d131adaeace110c9 Mon Sep 17 00:00:00 2001
From: David Whittaker <84562015+whitdog47@users.noreply.github.com>
Date: Fri, 25 Oct 2024 11:08:47 -0700
Subject: [PATCH 3/6] Fixes participant read through individual (#5396)
---
src/dispatch/case/flows.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/dispatch/case/flows.py b/src/dispatch/case/flows.py
index b0a2b0c9e457..bbd48bc9534f 100644
--- a/src/dispatch/case/flows.py
+++ b/src/dispatch/case/flows.py
@@ -336,7 +336,7 @@ def case_update_flow(
# we get the case
case = get(db_session=db_session, case_id=case_id)
- if reporter_email and case and reporter_email != case.reporter.email:
+ if reporter_email and case and reporter_email != case.reporter.individual.email:
# we run the case assign role flow for the reporter if it changed
case_assign_role_flow(
case_id=case.id,
@@ -345,7 +345,7 @@ def case_update_flow(
db_session=db_session,
)
- if assignee_email and case and assignee_email != case.assignee.email:
+ if assignee_email and case and assignee_email != case.assignee.individual.email:
# we run the case assign role flow for the assignee if it changed
case_assign_role_flow(
case_id=case.id,
@@ -374,7 +374,7 @@ def case_update_flow(
if case.tactical_group:
# we update the tactical group
- if reporter_email and reporter_email != case.reporter.email:
+ if reporter_email and reporter_email != case.reporter.individual.email:
group_flows.update_group(
subject=case,
group=case.tactical_group,
@@ -382,7 +382,7 @@ def case_update_flow(
group_member=reporter_email,
db_session=db_session,
)
- if assignee_email and assignee_email != case.assignee.email:
+ if assignee_email and assignee_email != case.assignee.individual.email:
group_flows.update_group(
subject=case,
group=case.tactical_group,
From 183eee4103d8966f170adf5529f7480abfb55400 Mon Sep 17 00:00:00 2001
From: David Whittaker <84562015+whitdog47@users.noreply.github.com>
Date: Fri, 25 Oct 2024 17:04:29 -0700
Subject: [PATCH 4/6] test(service_feedback): adding tests for oncall service
feedback (#5386)
---
tests/conftest.py | 6 +++
tests/factories.py | 33 ++++++++++++++++
tests/feedback/test_feedback_cases.py | 47 ++++++++++++++++++++++
tests/feedback/test_feedback_oncall.py | 55 ++++++++++++++++++++++++++
4 files changed, 141 insertions(+)
create mode 100644 tests/feedback/test_feedback_cases.py
create mode 100644 tests/feedback/test_feedback_oncall.py
diff --git a/tests/conftest.py b/tests/conftest.py
index 947bdaf79b1a..9f36dc7b37fa 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -62,6 +62,7 @@
ReportFactory,
SearchFilterFactory,
ServiceFactory,
+ ServiceFeedbackFactory,
SignalFactory,
SignalFilterFactory,
SignalInstanceFactory,
@@ -669,3 +670,8 @@ def cost_model(session):
@pytest.fixture
def cost_model_activity(session):
return CostModelActivityFactory()
+
+
+@pytest.fixture
+def service_feedback(session):
+ return ServiceFeedbackFactory()
diff --git a/tests/factories.py b/tests/factories.py
index 7ffbfc9815b3..eb225343be60 100644
--- a/tests/factories.py
+++ b/tests/factories.py
@@ -53,6 +53,8 @@
from dispatch.route.models import Recommendation, RecommendationMatch
from dispatch.search_filter.models import SearchFilter
from dispatch.service.models import Service
+from dispatch.feedback.service.models import ServiceFeedback
+from dispatch.feedback.service.enums import ServiceFeedbackRating
from dispatch.signal.models import Signal, SignalFilter, SignalInstance
from dispatch.storage.models import Storage
from dispatch.tag.models import Tag
@@ -1450,3 +1452,34 @@ def creator(self, create, extracted, **kwargs):
if extracted:
self.creator_id = extracted.id
+
+
+class ServiceFeedbackFactory(BaseFactory):
+ """Service Feedback Factory."""
+
+ rating = FuzzyChoice(
+ [
+ ServiceFeedbackRating.no_effort,
+ ServiceFeedbackRating.little_effort,
+ ServiceFeedbackRating.moderate_effort,
+ ServiceFeedbackRating.lots_of_effort,
+ ServiceFeedbackRating.very_high_effort,
+ ServiceFeedbackRating.extreme_effort,
+ ]
+ )
+ feedback = FuzzyText()
+ hours = FuzzyInteger(low=0, high=100)
+ project = SubFactory(ProjectFactory)
+
+ class Meta:
+ """Factory Configuration."""
+
+ model = ServiceFeedback
+
+ @post_generation
+ def individual(self, create, extracted, **kwargs):
+ if not create:
+ return
+
+ if extracted:
+ self.individual_id = extracted.id
diff --git a/tests/feedback/test_feedback_cases.py b/tests/feedback/test_feedback_cases.py
new file mode 100644
index 000000000000..9dbc6b66d648
--- /dev/null
+++ b/tests/feedback/test_feedback_cases.py
@@ -0,0 +1,47 @@
+def test_create(session, case, case_type, case_priority):
+ from dispatch.feedback.incident.service import create
+ from dispatch.feedback.incident.models import FeedbackCreate
+
+ case.incident_type = case_type
+ case.incident_priority = case_priority
+ rating = "Neither satisfied nor dissatisfied"
+ feedback = "The incident commander did an excellent job"
+
+ feedback_in = FeedbackCreate(rating=rating, feedback=feedback, case=case)
+ feedback = create(db_session=session, feedback_in=feedback_in)
+ assert feedback
+
+
+def test_get(session, feedback):
+ from dispatch.feedback.incident.service import get
+
+ t_feedback = get(db_session=session, feedback_id=feedback.id)
+ assert t_feedback.id == feedback.id
+
+
+def test_get_all(session, feedbacks):
+ from dispatch.feedback.incident.service import get_all
+
+ t_feedbacks = get_all(db_session=session).all()
+ assert t_feedbacks
+
+
+def test_update(session, feedback):
+ from dispatch.feedback.incident.service import update
+ from dispatch.feedback.incident.models import FeedbackUpdate
+
+ rating = "Very satisfied"
+ feedback_text = "The incident commander did an excellent job"
+
+ feedback_in = FeedbackUpdate(rating=rating, feedback=feedback_text)
+ feedback = update(db_session=session, feedback=feedback, feedback_in=feedback_in)
+
+ assert feedback.rating == rating
+ assert feedback.feedback == feedback_text
+
+
+def test_delete(session, feedback):
+ from dispatch.feedback.incident.service import delete, get
+
+ delete(db_session=session, feedback_id=feedback.id)
+ assert not get(db_session=session, feedback_id=feedback.id)
diff --git a/tests/feedback/test_feedback_oncall.py b/tests/feedback/test_feedback_oncall.py
new file mode 100644
index 000000000000..fd0cc9ae3efd
--- /dev/null
+++ b/tests/feedback/test_feedback_oncall.py
@@ -0,0 +1,55 @@
+""" Tests oncall service feedback """
+
+
+def test_create(session, participant, project):
+ from dispatch.feedback.service.service import create
+ from dispatch.feedback.service.models import ServiceFeedbackCreate
+
+ feedback = "Not a difficult shift"
+ hours = 5
+ rating = "No effort"
+
+ feedback_in = ServiceFeedbackCreate(
+ individual=participant.individual,
+ rating=rating,
+ feedback=feedback,
+ hours=hours,
+ project=project,
+ )
+ feedback = create(db_session=session, service_feedback_in=feedback_in)
+ assert feedback
+
+
+def test_get(session, service_feedback):
+ from dispatch.feedback.service.service import get
+
+ t_feedback = get(db_session=session, service_feedback_id=service_feedback.id)
+ assert t_feedback.id == service_feedback.id
+
+
+def test_get_all(session):
+ from dispatch.feedback.service.service import get_all
+
+ t_feedbacks = get_all(db_session=session).all()
+ assert t_feedbacks
+
+
+def test_update(session, service_feedback):
+ from dispatch.feedback.service.service import update
+ from dispatch.feedback.service.models import ServiceFeedbackUpdate
+
+ feedback_text = "Changed my mind. The shift was difficult"
+
+ feedback_in = ServiceFeedbackUpdate(id=service_feedback.id, feedback=feedback_text)
+ feedback = update(
+ db_session=session, service_feedback=service_feedback, service_feedback_in=feedback_in
+ )
+
+ assert feedback.feedback == feedback_text
+
+
+def test_delete(session, service_feedback):
+ from dispatch.feedback.service.service import delete, get
+
+ delete(db_session=session, service_feedback_id=service_feedback.id)
+ assert not get(db_session=session, service_feedback_id=service_feedback.id)
From 2dde1cc280ffde0e549199a9564c8a7e88a536cf Mon Sep 17 00:00:00 2001
From: David Whittaker <84562015+whitdog47@users.noreply.github.com>
Date: Fri, 25 Oct 2024 17:10:15 -0700
Subject: [PATCH 5/6] feat(case): add case feedback daily email (#5395)
---
src/dispatch/feedback/incident/messaging.py | 56 ++++++++++++++++++++-
src/dispatch/feedback/incident/scheduled.py | 47 +++++++++++++----
src/dispatch/feedback/incident/service.py | 17 ++++++-
src/dispatch/messaging/email/utils.py | 5 ++
src/dispatch/messaging/strings.py | 15 ++++++
5 files changed, 126 insertions(+), 14 deletions(-)
diff --git a/src/dispatch/feedback/incident/messaging.py b/src/dispatch/feedback/incident/messaging.py
index b32b9daa4d02..470189ea62cc 100644
--- a/src/dispatch/feedback/incident/messaging.py
+++ b/src/dispatch/feedback/incident/messaging.py
@@ -1,9 +1,11 @@
import logging
from typing import List
-from dispatch.database.core import SessionLocal
+from sqlalchemy.orm import Session
+
from dispatch.messaging.strings import (
INCIDENT_FEEDBACK_DAILY_REPORT,
+ CASE_FEEDBACK_DAILY_REPORT,
MessageType,
)
from dispatch.plugin import service as plugin_service
@@ -15,7 +17,7 @@
def send_incident_feedback_daily_report(
- commander_email: str, feedback: List[Feedback], project_id: int, db_session: SessionLocal
+ commander_email: str, feedback: List[Feedback], project_id: int, db_session: Session
):
"""Sends an incident feedback daily report to all incident commanders who received feedback."""
plugin = plugin_service.get_active_instance(
@@ -62,3 +64,53 @@ def send_incident_feedback_daily_report(
log.error(f"Error in sending {notification_text} email to {commander_email}: {e}")
log.debug(f"Incident feedback daily report sent to {commander_email}.")
+
+
+def send_case_feedback_daily_report(
+ assignee_email: str, feedback: List[Feedback], project_id: int, db_session: Session
+):
+ """Sends an case feedback daily report to all case assignees who received feedback."""
+ plugin = plugin_service.get_active_instance(
+ db_session=db_session, project_id=project_id, plugin_type="email"
+ )
+
+ if not plugin:
+ log.warning("Case feedback daily report not sent. Email plugin is not enabled.")
+ return
+
+ items = []
+ for piece in feedback:
+ participant = piece.participant.individual.name if piece.participant else "Anonymous"
+ items.append(
+ {
+ "name": piece.case.name,
+ "title": piece.case.title,
+ "rating": piece.rating,
+ "feedback": piece.feedback,
+ "participant": participant,
+ "created_at": piece.created_at,
+ }
+ )
+
+ name = subject = notification_text = "Case Feedback Daily Report"
+ assignee_fullname = feedback[0].case.assignee.individual.name
+ assignee_weblink = feedback[0].case.assignee.individual.weblink
+
+ # Can raise exception "tenacity.RetryError: RetryError". (Email may still go through).
+ try:
+ plugin.instance.send(
+ assignee_email,
+ notification_text,
+ CASE_FEEDBACK_DAILY_REPORT,
+ MessageType.case_feedback_daily_report,
+ name=name,
+ subject=subject,
+ cc=plugin.project.owner_email,
+ items=items,
+ contact_fullname=assignee_fullname,
+ contact_weblink=assignee_weblink,
+ )
+ except Exception as e:
+ log.error(f"Error in sending {notification_text} email to {assignee_email}: {e}")
+
+ log.debug(f"Case feedback daily report sent to {assignee_email}.")
diff --git a/src/dispatch/feedback/incident/scheduled.py b/src/dispatch/feedback/incident/scheduled.py
index 63c202de5edf..0b0696acd736 100644
--- a/src/dispatch/feedback/incident/scheduled.py
+++ b/src/dispatch/feedback/incident/scheduled.py
@@ -2,13 +2,17 @@
from schedule import every
import logging
-from dispatch.database.core import SessionLocal
+from sqlalchemy.orm import Session
+
from dispatch.decorators import scheduled_project_task, timer
from dispatch.project.models import Project
from dispatch.scheduler import scheduler
-from .messaging import send_incident_feedback_daily_report
-from .service import get_all_last_x_hours_by_project_id
+from .messaging import send_incident_feedback_daily_report, send_case_feedback_daily_report
+from .service import (
+ get_all_incident_last_x_hours_by_project_id,
+ get_all_case_last_x_hours_by_project_id,
+)
log = logging.getLogger(__name__)
@@ -17,21 +21,42 @@ def group_feedback_by_commander(feedback):
"""Groups feedback by commander."""
grouped = defaultdict(lambda: [])
for piece in feedback:
- grouped[piece.incident.commander.individual.email].append(piece)
+ if piece.incident and piece.incident.commander:
+ grouped[piece.incident.commander.individual.email].append(piece)
+ return grouped
+
+
+def group_feedback_by_assignee(feedback):
+ """Groups feedback by assignee."""
+ grouped = defaultdict(lambda: [])
+ for piece in feedback:
+ if piece.case and piece.case.assignee:
+ grouped[piece.case.assignee.individual.email].append(piece)
return grouped
@scheduler.add(every(1).day.at("18:00"), name="feedback-report-daily")
@timer
@scheduled_project_task
-def feedback_report_daily(db_session: SessionLocal, project: Project):
+def feedback_report_daily(db_session: Session, project: Project):
"""
- Fetches all incident feedback provided in the last 24 hours
- and sends a daily report to the commanders who handled the incidents.
+ Fetches all incident and case feedback provided in the last 24 hours
+ and sends a daily report to the commanders and assignees who handled the incidents/cases.
"""
- feedback = get_all_last_x_hours_by_project_id(db_session=db_session, project_id=project.id)
+ incident_feedback = get_all_incident_last_x_hours_by_project_id(
+ db_session=db_session, project_id=project.id
+ )
- if feedback:
- grouped_feedback = group_feedback_by_commander(feedback)
- for commander_email, feedback in grouped_feedback.items():
+ if incident_feedback:
+ grouped_incident_feedback = group_feedback_by_commander(incident_feedback)
+ for commander_email, feedback in grouped_incident_feedback.items():
send_incident_feedback_daily_report(commander_email, feedback, project.id, db_session)
+
+ case_feedback = get_all_case_last_x_hours_by_project_id(
+ db_session=db_session, project_id=project.id
+ )
+
+ if case_feedback:
+ grouped_case_feedback = group_feedback_by_assignee(case_feedback)
+ for assignee_email, feedback in grouped_case_feedback.items():
+ send_case_feedback_daily_report(assignee_email, feedback, project.id, db_session)
diff --git a/src/dispatch/feedback/incident/service.py b/src/dispatch/feedback/incident/service.py
index fd80be90909a..e9aa646b5035 100644
--- a/src/dispatch/feedback/incident/service.py
+++ b/src/dispatch/feedback/incident/service.py
@@ -4,6 +4,7 @@
from dispatch.incident import service as incident_service
from dispatch.case import service as case_service
from dispatch.incident.models import Incident
+from dispatch.case.models import Case
from dispatch.project.models import Project
from .models import Feedback, FeedbackCreate, FeedbackUpdate
@@ -19,7 +20,7 @@ def get_all(*, db_session):
return db_session.query(Feedback)
-def get_all_last_x_hours_by_project_id(
+def get_all_incident_last_x_hours_by_project_id(
*, db_session, hours: int = 24, project_id: int
) -> List[Optional[Feedback]]:
"""Returns all feedback provided in the last x hours by project id. Defaults to 24 hours."""
@@ -33,6 +34,20 @@ def get_all_last_x_hours_by_project_id(
)
+def get_all_case_last_x_hours_by_project_id(
+ *, db_session, hours: int = 24, project_id: int
+) -> List[Optional[Feedback]]:
+ """Returns all feedback provided in the last x hours by project id. Defaults to 24 hours."""
+ return (
+ db_session.query(Feedback)
+ .join(Case)
+ .join(Project)
+ .filter(Project.id == project_id)
+ .filter(Feedback.created_at >= datetime.utcnow() - timedelta(hours=hours))
+ .all()
+ )
+
+
def create(*, db_session, feedback_in: FeedbackCreate) -> Feedback:
"""Creates a new piece of feedback."""
if feedback_in.incident:
diff --git a/src/dispatch/messaging/email/utils.py b/src/dispatch/messaging/email/utils.py
index 0b71dc6f9912..19aed7dfebaa 100644
--- a/src/dispatch/messaging/email/utils.py
+++ b/src/dispatch/messaging/email/utils.py
@@ -12,6 +12,7 @@
INCIDENT_DAILY_REPORT_DESCRIPTION,
INCIDENT_FEEDBACK_DAILY_REPORT_DESCRIPTION,
INCIDENT_TASK_REMINDER_DESCRIPTION,
+ CASE_FEEDBACK_DAILY_REPORT_DESCRIPTION,
MessageType,
render_message_template,
)
@@ -42,6 +43,10 @@ def get_template(message_type: MessageType, project_id: int):
"notification_list.mjml",
INCIDENT_FEEDBACK_DAILY_REPORT_DESCRIPTION,
),
+ MessageType.case_feedback_daily_report: (
+ "notification_list.mjml",
+ CASE_FEEDBACK_DAILY_REPORT_DESCRIPTION,
+ ),
MessageType.incident_daily_report: (
"notification_list.mjml",
INCIDENT_DAILY_REPORT_DESCRIPTION,
diff --git a/src/dispatch/messaging/strings.py b/src/dispatch/messaging/strings.py
index 70e0353d979d..b98cfc7e26a9 100644
--- a/src/dispatch/messaging/strings.py
+++ b/src/dispatch/messaging/strings.py
@@ -43,6 +43,7 @@ class MessageType(DispatchEnum):
service_feedback = "service-feedback"
task_add_to_incident = "task-add-to-incident"
case_rating_feedback = "case-rating-feedback"
+ case_feedback_daily_report = "case-feedback-daily-report"
INCIDENT_STATUS_DESCRIPTIONS = {
@@ -80,6 +81,11 @@ class MessageType(DispatchEnum):
"\n", " "
).strip()
+CASE_FEEDBACK_DAILY_REPORT_DESCRIPTION = """
+This is a daily report of feedback about cases handled by you.""".replace(
+ "\n", " "
+).strip()
+
INCIDENT_WEEKLY_REPORT_TITLE = """
Incidents Weekly Report""".replace(
"\n", " "
@@ -999,6 +1005,15 @@ class MessageType(DispatchEnum):
{"title": "Created At", "text": "", "datetime": "{{ created_at}}"},
]
+CASE_FEEDBACK_DAILY_REPORT = [
+ {"title": "Case", "text": "{{ name }}"},
+ {"title": "Case Title", "text": "{{ title }}"},
+ {"title": "Rating", "text": "{{ rating }}"},
+ {"title": "Feedback", "text": "{{ feedback }}"},
+ {"title": "Participant", "text": "{{ participant }}"},
+ {"title": "Created At", "text": "", "datetime": "{{ created_at}}"},
+]
+
INCIDENT_WEEKLY_REPORT_HEADER = {
"type": "header",
"text": INCIDENT_WEEKLY_REPORT_TITLE,
From 8c7742a70d3b76c89829ad2c5eaf2a4b378a3f9b Mon Sep 17 00:00:00 2001
From: David Whittaker <84562015+whitdog47@users.noreply.github.com>
Date: Fri, 25 Oct 2024 17:17:03 -0700
Subject: [PATCH 6/6] Ensure case tickets are updated (#5367)
---
src/dispatch/case/flows.py | 3 +++
.../plugins/dispatch_slack/case/interactive.py | 13 +++++++++++++
2 files changed, 16 insertions(+)
diff --git a/src/dispatch/case/flows.py b/src/dispatch/case/flows.py
index bbd48bc9534f..14c74e619e5d 100644
--- a/src/dispatch/case/flows.py
+++ b/src/dispatch/case/flows.py
@@ -488,6 +488,9 @@ def case_closed_status_flow(case: Case, db_session=None):
if not storage_plugin:
return
+ # we update the ticket
+ ticket_flows.update_case_ticket(case=case, db_session=db_session)
+
# Open document access if configured
if storage_plugin.configuration.open_on_close:
for document in case.documents:
diff --git a/src/dispatch/plugins/dispatch_slack/case/interactive.py b/src/dispatch/plugins/dispatch_slack/case/interactive.py
index 08500ff06d88..51019e53bb98 100644
--- a/src/dispatch/plugins/dispatch_slack/case/interactive.py
+++ b/src/dispatch/plugins/dispatch_slack/case/interactive.py
@@ -106,6 +106,7 @@
SignalFilterCreate,
SignalInstance,
)
+from dispatch.ticket import flows as ticket_flows
log = logging.getLogger(__name__)
@@ -1008,6 +1009,10 @@ def handle_case_participant_role_activity(
organization_slug=context["subject"].organization_slug,
)
case.status = CaseStatus.triage
+
+ # we update the ticket
+ ticket_flows.update_case_ticket(case=case, db_session=db_session)
+
case_flows.update_conversation(case, db_session)
db_session.commit()
@@ -1073,6 +1078,10 @@ def reopen_button_click(
ack()
case = case_service.get(db_session=db_session, case_id=context["subject"].id)
case.status = CaseStatus.triage
+
+ # we update the ticket
+ ticket_flows.update_case_ticket(case=case, db_session=db_session)
+
db_session.commit()
# update case message
@@ -1644,6 +1653,10 @@ def triage_button_click(
)
case.status = CaseStatus.triage
db_session.commit()
+
+ # we update the ticket
+ ticket_flows.update_case_ticket(case=case, db_session=db_session)
+
case_flows.update_conversation(case, db_session)