firefighter-incident 0.0.22__py3-none-any.whl → 0.0.23__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- firefighter/_version.py +2 -2
- firefighter/api/serializers.py +18 -0
- firefighter/api/views/incidents.py +3 -0
- firefighter/confluence/models.py +66 -6
- firefighter/confluence/signals/incident_updated.py +8 -26
- firefighter/firefighter/settings/components/jira_app.py +33 -0
- firefighter/incidents/admin.py +3 -0
- firefighter/incidents/models/impact.py +3 -5
- firefighter/incidents/models/incident.py +24 -9
- firefighter/incidents/views/views.py +2 -0
- firefighter/jira_app/admin.py +15 -1
- firefighter/jira_app/apps.py +3 -0
- firefighter/jira_app/client.py +151 -3
- firefighter/jira_app/management/__init__.py +1 -0
- firefighter/jira_app/management/commands/__init__.py +1 -0
- firefighter/jira_app/migrations/0002_add_jira_postmortem_model.py +71 -0
- firefighter/jira_app/models.py +50 -0
- firefighter/jira_app/service_postmortem.py +292 -0
- firefighter/jira_app/signals/__init__.py +10 -0
- firefighter/jira_app/signals/incident_key_events_updated.py +88 -0
- firefighter/jira_app/signals/postmortem_created.py +155 -0
- firefighter/jira_app/templates/jira/postmortem/impact.txt +12 -0
- firefighter/jira_app/templates/jira/postmortem/incident_summary.txt +17 -0
- firefighter/jira_app/templates/jira/postmortem/mitigation_actions.txt +9 -0
- firefighter/jira_app/templates/jira/postmortem/root_causes.txt +12 -0
- firefighter/jira_app/templates/jira/postmortem/timeline.txt +7 -0
- firefighter/raid/signals/incident_updated.py +31 -11
- firefighter/slack/messages/slack_messages.py +39 -3
- firefighter/slack/signals/postmortem_created.py +51 -3
- firefighter/slack/views/modals/closure_reason.py +15 -0
- firefighter/slack/views/modals/key_event_message.py +9 -0
- firefighter/slack/views/modals/postmortem.py +32 -40
- firefighter/slack/views/modals/update_status.py +7 -1
- {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/METADATA +1 -1
- {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/RECORD +50 -31
- {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/WHEEL +1 -1
- firefighter_tests/test_api/test_renderer.py +41 -0
- firefighter_tests/test_incidents/test_models/test_incident_model.py +29 -0
- firefighter_tests/test_jira_app/test_incident_key_events_sync.py +112 -0
- firefighter_tests/test_jira_app/test_models.py +138 -0
- firefighter_tests/test_jira_app/test_postmortem_issue_link.py +201 -0
- firefighter_tests/test_jira_app/test_postmortem_service.py +416 -0
- firefighter_tests/test_jira_app/test_timeline_template.py +135 -0
- firefighter_tests/test_raid/test_raid_signals.py +50 -8
- firefighter_tests/test_slack/messages/test_slack_messages.py +112 -23
- firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +18 -2
- firefighter_tests/test_slack/views/modals/test_key_event_message.py +30 -0
- firefighter_tests/test_slack/views/modals/test_update_status.py +161 -129
- {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/entry_points.txt +0 -0
- {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/licenses/LICENSE +0 -0
|
@@ -24,16 +24,36 @@ def incident_updated_close_ticket_when_mitigated_or_postmortem(
|
|
|
24
24
|
updated_fields: list[str],
|
|
25
25
|
**kwargs: Any,
|
|
26
26
|
) -> None:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
"""Close Jira incident ticket based on incident status and priority.
|
|
28
|
+
|
|
29
|
+
Closure logic:
|
|
30
|
+
- P1/P2 (needs_postmortem): Close only when incident is CLOSED
|
|
31
|
+
- P3+ (no postmortem): Close when incident is MITIGATED or CLOSED
|
|
32
|
+
- POST_MORTEM status never closes the ticket (it remains open during PM phase)
|
|
33
|
+
"""
|
|
34
|
+
if "_status" not in updated_fields:
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
if not hasattr(incident, "jira_ticket") or incident.jira_ticket is None:
|
|
38
|
+
logger.warning(
|
|
39
|
+
f"Trying to close Jira ticket for incident {incident.id} but no Jira ticket found"
|
|
40
|
+
)
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
# Determine if we should close the ticket based on status and priority
|
|
44
|
+
should_close = False
|
|
45
|
+
|
|
46
|
+
if incident_update.status == IncidentStatus.CLOSED:
|
|
47
|
+
# Always close on CLOSED regardless of priority
|
|
48
|
+
should_close = True
|
|
49
|
+
elif incident_update.status == IncidentStatus.MITIGATED:
|
|
50
|
+
# Only close on MITIGATED if incident doesn't need postmortem (P3+)
|
|
51
|
+
should_close = not incident.needs_postmortem
|
|
52
|
+
|
|
53
|
+
# POST_MORTEM status never closes the ticket - it stays open during PM phase
|
|
54
|
+
|
|
55
|
+
if should_close:
|
|
56
|
+
status_label = incident_update.status.label if incident_update.status else "Unknown"
|
|
57
|
+
logger.info(f"Closing Jira ticket for incident {incident.id} (status: {status_label})")
|
|
38
58
|
client.close_issue(issue_id=incident.jira_ticket.id)
|
|
39
59
|
# XXX We may want to add a comment if there is an incident update message on close
|
|
@@ -208,6 +208,7 @@ class SlackMessageIncidentDeclaredAnnouncement(SlackMessageSurface):
|
|
|
208
208
|
return f"A new {self.incident.priority} incident has been declared: {self.incident.title}"
|
|
209
209
|
|
|
210
210
|
def get_blocks(self) -> list[Block]:
|
|
211
|
+
# Build main fields (keep under 10 items due to Slack limit)
|
|
211
212
|
fields = [
|
|
212
213
|
f"{self.incident.priority.emoji} *Priority:* {self.incident.priority.name}",
|
|
213
214
|
f":package: *Incident category:* {self.incident.incident_category.name}",
|
|
@@ -218,7 +219,7 @@ class SlackMessageIncidentDeclaredAnnouncement(SlackMessageSurface):
|
|
|
218
219
|
if hasattr(self.incident, "jira_ticket") and self.incident.jira_ticket:
|
|
219
220
|
fields.append(f":jira_new: <{self.incident.jira_ticket.url}|*Jira ticket*>")
|
|
220
221
|
|
|
221
|
-
# Add custom fields if present
|
|
222
|
+
# Add custom fields if present (max to avoid exceeding 10 fields limit)
|
|
222
223
|
if hasattr(self.incident, "custom_fields") and self.incident.custom_fields:
|
|
223
224
|
custom_fields = self.incident.custom_fields
|
|
224
225
|
if custom_fields.get("zendesk_ticket_id"):
|
|
@@ -240,17 +241,36 @@ class SlackMessageIncidentDeclaredAnnouncement(SlackMessageSurface):
|
|
|
240
241
|
slack_block_quote(self.incident.description),
|
|
241
242
|
DividerBlock(),
|
|
242
243
|
SectionBlock(
|
|
243
|
-
fields=fields,
|
|
244
|
+
fields=fields[:10], # Slack limits fields to 10 items max
|
|
244
245
|
accessory=ButtonElement(
|
|
245
246
|
text="Update",
|
|
246
247
|
value=str(self.incident.id),
|
|
247
248
|
action_id=UpdateModal.open_action,
|
|
248
249
|
),
|
|
249
250
|
),
|
|
251
|
+
*self._postmortem_blocks(),
|
|
250
252
|
*self._impact_blocks(),
|
|
251
253
|
]
|
|
252
254
|
return blocks
|
|
253
255
|
|
|
256
|
+
def _postmortem_blocks(self) -> list[Block]:
|
|
257
|
+
"""Build post-mortem links block if any PM exists."""
|
|
258
|
+
pm_fields = []
|
|
259
|
+
|
|
260
|
+
if hasattr(self.incident, "postmortem_for") and self.incident.postmortem_for:
|
|
261
|
+
pm_fields.append(f":confluence: <{self.incident.postmortem_for.page_url}|*Confluence Post-mortem*>")
|
|
262
|
+
|
|
263
|
+
if hasattr(self.incident, "jira_postmortem_for") and self.incident.jira_postmortem_for:
|
|
264
|
+
pm_fields.append(f":jira_new: <{self.incident.jira_postmortem_for.issue_url}|*Jira Post-mortem ({self.incident.jira_postmortem_for.jira_issue_key})*>")
|
|
265
|
+
|
|
266
|
+
if not pm_fields:
|
|
267
|
+
return []
|
|
268
|
+
|
|
269
|
+
return [
|
|
270
|
+
DividerBlock(),
|
|
271
|
+
SectionBlock(fields=pm_fields),
|
|
272
|
+
]
|
|
273
|
+
|
|
254
274
|
def _impact_blocks(self) -> list[Block]:
|
|
255
275
|
impacts = self.incident.impacts.all().order_by("impact_level__value")[:10]
|
|
256
276
|
none_level = LevelChoices.NONE.value
|
|
@@ -573,7 +593,23 @@ class SlackMessageIncidentPostMortemCreated(SlackMessageSurface):
|
|
|
573
593
|
super().__init__()
|
|
574
594
|
|
|
575
595
|
def get_text(self) -> str:
|
|
576
|
-
|
|
596
|
+
"""Generate text with links to all available post-mortems."""
|
|
597
|
+
parts = ["📔 The post-mortem has been created:"]
|
|
598
|
+
|
|
599
|
+
# Add Confluence link if available
|
|
600
|
+
if hasattr(self.incident, "postmortem_for"):
|
|
601
|
+
parts.append(
|
|
602
|
+
f"• Confluence: {self.incident.postmortem_for.page_url}"
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
# Add Jira link if available
|
|
606
|
+
if hasattr(self.incident, "jira_postmortem_for"):
|
|
607
|
+
jira_pm = self.incident.jira_postmortem_for
|
|
608
|
+
parts.append(
|
|
609
|
+
f"• Jira: {jira_pm.issue_url} ({jira_pm.jira_issue_key})"
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
return "\n".join(parts)
|
|
577
613
|
|
|
578
614
|
def get_blocks(self) -> list[Block]:
|
|
579
615
|
return [SectionBlock(text=self.get_text())]
|
|
@@ -4,6 +4,7 @@ import logging
|
|
|
4
4
|
from typing import TYPE_CHECKING, Any
|
|
5
5
|
|
|
6
6
|
from django.dispatch.dispatcher import receiver
|
|
7
|
+
from slack_sdk.errors import SlackApiError
|
|
7
8
|
|
|
8
9
|
from firefighter.incidents.signals import postmortem_created
|
|
9
10
|
from firefighter.slack.messages.slack_messages import (
|
|
@@ -19,7 +20,19 @@ logger = logging.getLogger(__name__)
|
|
|
19
20
|
@receiver(signal=postmortem_created)
|
|
20
21
|
# pylint: disable=unused-argument
|
|
21
22
|
def postmortem_created_send(sender: Any, incident: Incident, **kwargs: Any) -> None:
|
|
22
|
-
|
|
23
|
+
# Refresh incident from database to get the newly created post-mortem relationships
|
|
24
|
+
# This is necessary because the signal might be sent before the ORM cache is updated
|
|
25
|
+
try:
|
|
26
|
+
incident.refresh_from_db()
|
|
27
|
+
except Exception:
|
|
28
|
+
logger.exception(f"Failed to refresh incident #{incident.id} from database")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
# Check if at least one post-mortem exists
|
|
32
|
+
has_confluence = hasattr(incident, "postmortem_for")
|
|
33
|
+
has_jira = hasattr(incident, "jira_postmortem_for")
|
|
34
|
+
|
|
35
|
+
if not has_confluence and not has_jira:
|
|
23
36
|
logger.warning(f"No PostMortem to post for incident {incident}.")
|
|
24
37
|
return
|
|
25
38
|
|
|
@@ -28,9 +41,44 @@ def postmortem_created_send(sender: Any, incident: Incident, **kwargs: Any) -> N
|
|
|
28
41
|
f"No Incident Slack channel to post PostMortem for incident {incident}."
|
|
29
42
|
)
|
|
30
43
|
return
|
|
44
|
+
|
|
45
|
+
# Send message with all available post-mortem links (pinned)
|
|
31
46
|
incident.conversation.send_message_and_save(
|
|
32
47
|
SlackMessageIncidentPostMortemCreated(incident), pin=True
|
|
33
48
|
)
|
|
34
|
-
|
|
35
|
-
|
|
49
|
+
|
|
50
|
+
# Update the initial incident message with post-mortem links
|
|
51
|
+
from firefighter.slack.messages.slack_messages import ( # noqa: PLC0415
|
|
52
|
+
SlackMessageIncidentDeclaredAnnouncement,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
incident.conversation.send_message_and_save(
|
|
56
|
+
SlackMessageIncidentDeclaredAnnouncement(incident)
|
|
36
57
|
)
|
|
58
|
+
|
|
59
|
+
# Add bookmarks for each available post-mortem
|
|
60
|
+
if has_confluence:
|
|
61
|
+
try:
|
|
62
|
+
incident.conversation.add_bookmark(
|
|
63
|
+
title="Postmortem (Confluence)",
|
|
64
|
+
link=incident.postmortem_for.page_url,
|
|
65
|
+
emoji=":confluence:",
|
|
66
|
+
)
|
|
67
|
+
except SlackApiError as e:
|
|
68
|
+
logger.warning(
|
|
69
|
+
f"Failed to add Confluence bookmark for incident #{incident.id}: {e}. "
|
|
70
|
+
"This is expected in test environments without custom emojis."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if has_jira:
|
|
74
|
+
try:
|
|
75
|
+
incident.conversation.add_bookmark(
|
|
76
|
+
title=f"Postmortem ({incident.jira_postmortem_for.jira_issue_key})",
|
|
77
|
+
link=incident.jira_postmortem_for.issue_url,
|
|
78
|
+
emoji=":jira_new:",
|
|
79
|
+
)
|
|
80
|
+
except SlackApiError as e:
|
|
81
|
+
logger.warning(
|
|
82
|
+
f"Failed to add Jira bookmark for incident #{incident.id}: {e}. "
|
|
83
|
+
"This is expected in test environments without custom emojis."
|
|
84
|
+
)
|
|
@@ -115,6 +115,21 @@ class ClosureReasonModal(IncidentSelectableModalMixin, SlackModal):
|
|
|
115
115
|
self, ack: Ack, body: dict[str, Any], incident: Incident, user: User
|
|
116
116
|
) -> bool | None:
|
|
117
117
|
"""Handle the closure reason modal submission."""
|
|
118
|
+
# Early validation: Check if incident can be closed BEFORE calling ack()
|
|
119
|
+
# This validation happens BEFORE ack() so we can display errors in the modal
|
|
120
|
+
can_close, reasons = incident.can_be_closed
|
|
121
|
+
if not can_close:
|
|
122
|
+
# Build error message from reasons
|
|
123
|
+
error_messages = [reason[1] for reason in reasons]
|
|
124
|
+
error_text = "\n".join([f"• {msg}" for msg in error_messages])
|
|
125
|
+
ack(
|
|
126
|
+
response_action="errors",
|
|
127
|
+
errors={
|
|
128
|
+
"closure_message": f"Cannot close this incident:\n{error_text}"
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
return False
|
|
132
|
+
|
|
118
133
|
# Clear ALL modals in the stack (not just this one)
|
|
119
134
|
# This ensures the underlying "Update Status" modal is also closed
|
|
120
135
|
ack(response_action="clear")
|
|
@@ -12,6 +12,7 @@ from slack_sdk.models.blocks.basic_components import MarkdownTextObject
|
|
|
12
12
|
from slack_sdk.models.blocks.block_elements import ButtonElement
|
|
13
13
|
|
|
14
14
|
from firefighter.incidents.forms.update_key_events import IncidentUpdateKeyEventsForm
|
|
15
|
+
from firefighter.incidents.signals import incident_key_events_updated
|
|
15
16
|
from firefighter.slack.messages.base import SlackMessageStrategy, SlackMessageSurface
|
|
16
17
|
from firefighter.slack.views.modals.base_modal.base import MessageForm
|
|
17
18
|
|
|
@@ -108,6 +109,14 @@ class KeyEvents(MessageForm[IncidentUpdateKeyEventsForm]):
|
|
|
108
109
|
return
|
|
109
110
|
self.form = form
|
|
110
111
|
self.form.save()
|
|
112
|
+
|
|
113
|
+
# Send signal to update Jira post-mortem timeline if applicable
|
|
114
|
+
logger.debug("Sending signal incident_key_events_updated")
|
|
115
|
+
incident_key_events_updated.send_robust(
|
|
116
|
+
__name__,
|
|
117
|
+
incident=incident,
|
|
118
|
+
)
|
|
119
|
+
|
|
111
120
|
incident.compute_metrics()
|
|
112
121
|
|
|
113
122
|
self.update_with_form()
|
|
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
import logging
|
|
4
4
|
from typing import TYPE_CHECKING, Any
|
|
5
5
|
|
|
6
|
-
from django.apps import apps
|
|
7
6
|
from slack_sdk.models.blocks.blocks import Block, SectionBlock
|
|
8
7
|
from slack_sdk.models.views import View
|
|
9
8
|
|
|
@@ -12,9 +11,6 @@ from firefighter.slack.views.modals.base_modal.mixins import (
|
|
|
12
11
|
IncidentSelectableModalMixin,
|
|
13
12
|
)
|
|
14
13
|
|
|
15
|
-
if apps.is_installed("firefighter.confluence"):
|
|
16
|
-
from firefighter.confluence.models import PostMortem
|
|
17
|
-
|
|
18
14
|
if TYPE_CHECKING:
|
|
19
15
|
from slack_bolt.context.ack.ack import Ack
|
|
20
16
|
|
|
@@ -34,54 +30,50 @@ class PostMortemModal(
|
|
|
34
30
|
|
|
35
31
|
def build_modal_fn(self, incident: Incident, **kwargs: Any) -> View:
|
|
36
32
|
blocks: list[Block] = []
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
|
|
33
|
+
|
|
34
|
+
# Check existing post-mortems
|
|
35
|
+
has_confluence = hasattr(incident, "postmortem_for")
|
|
36
|
+
has_jira = hasattr(incident, "jira_postmortem_for")
|
|
37
|
+
|
|
38
|
+
if has_confluence or has_jira:
|
|
39
|
+
blocks.append(
|
|
40
|
+
SectionBlock(text=f"Post-mortem(s) for incident #{incident.id}:")
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if has_confluence:
|
|
44
|
+
blocks.append(
|
|
45
|
+
SectionBlock(
|
|
46
|
+
text=f"• Confluence: <{incident.postmortem_for.page_url}|View page>"
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if has_jira:
|
|
51
|
+
blocks.append(
|
|
52
|
+
SectionBlock(
|
|
53
|
+
text=f"• Jira: <{incident.jira_postmortem_for.issue_url}|{incident.jira_postmortem_for.jira_issue_key}>"
|
|
54
|
+
)
|
|
55
|
+
)
|
|
47
56
|
else:
|
|
48
|
-
blocks.
|
|
57
|
+
blocks.append(
|
|
49
58
|
SectionBlock(
|
|
50
|
-
text=f"
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
text="Click on the button to create the postmortem on Confluence."
|
|
54
|
-
),
|
|
55
|
-
])
|
|
56
|
-
submit = "Create postmortem"[:24]
|
|
59
|
+
text=f"Post-mortem for incident #{incident.id} will be automatically created when the incident reaches MITIGATED status."
|
|
60
|
+
)
|
|
61
|
+
)
|
|
57
62
|
|
|
58
63
|
return View(
|
|
59
64
|
type="modal",
|
|
60
65
|
title="Postmortem"[:24],
|
|
61
|
-
submit=
|
|
66
|
+
submit=None,
|
|
62
67
|
callback_id=self.callback_id,
|
|
63
68
|
private_metadata=str(incident.id),
|
|
64
69
|
blocks=blocks,
|
|
65
70
|
)
|
|
66
71
|
|
|
67
72
|
@staticmethod
|
|
68
|
-
def handle_modal_fn(ack: Ack,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if hasattr(incident, "postmortem_for"):
|
|
73
|
-
ack(text="Post-mortem has already been created.")
|
|
74
|
-
return
|
|
75
|
-
|
|
76
|
-
# Check if this modal was pushed on top of another modal
|
|
77
|
-
# If yes, clear the entire stack to avoid leaving stale modals visible
|
|
78
|
-
is_pushed = body.get("view", {}).get("previous_view_id") is not None
|
|
79
|
-
if is_pushed:
|
|
80
|
-
ack(response_action="clear")
|
|
81
|
-
else:
|
|
82
|
-
ack()
|
|
83
|
-
|
|
84
|
-
PostMortem.objects.create_postmortem_for_incident(incident)
|
|
73
|
+
def handle_modal_fn(ack: Ack, **_kwargs: Any) -> None:
|
|
74
|
+
# This modal is now read-only (no submit button)
|
|
75
|
+
# Post-mortems are created automatically when incident reaches MITIGATED status
|
|
76
|
+
ack()
|
|
85
77
|
|
|
86
78
|
|
|
87
79
|
modal_postmortem = PostMortemModal()
|
|
@@ -104,12 +104,14 @@ class UpdateStatusModal(ModalForm[UpdateStatusFormSlack]):
|
|
|
104
104
|
},
|
|
105
105
|
"incident": incident,
|
|
106
106
|
},
|
|
107
|
+
ack_on_success=False, # We'll ack after custom validations
|
|
107
108
|
)
|
|
108
109
|
if slack_form is None:
|
|
109
110
|
return
|
|
110
111
|
form: UpdateStatusFormSlack = slack_form.form
|
|
111
112
|
if len(form.cleaned_data) == 0:
|
|
112
113
|
# XXX We should have a prompt for empty forms
|
|
114
|
+
ack()
|
|
113
115
|
return
|
|
114
116
|
|
|
115
117
|
# Check if user is trying to close and needs a closure reason
|
|
@@ -118,13 +120,14 @@ class UpdateStatusModal(ModalForm[UpdateStatusFormSlack]):
|
|
|
118
120
|
if handle_update_status_close_request(ack, body, incident, target_status):
|
|
119
121
|
return
|
|
120
122
|
|
|
121
|
-
#
|
|
123
|
+
# Validate that incident can be closed (check key events, post-mortem, etc.)
|
|
122
124
|
if target_status == IncidentStatus.CLOSED:
|
|
123
125
|
can_close, reasons = incident.can_be_closed
|
|
124
126
|
if not can_close:
|
|
125
127
|
# Build error message from reasons
|
|
126
128
|
error_messages = [reason[1] for reason in reasons]
|
|
127
129
|
error_text = "\n".join([f"• {msg}" for msg in error_messages])
|
|
130
|
+
logger.warning(f"Cannot close incident #{incident.id} via Update Status: {error_text}")
|
|
128
131
|
ack(
|
|
129
132
|
response_action="errors",
|
|
130
133
|
errors={
|
|
@@ -133,6 +136,9 @@ class UpdateStatusModal(ModalForm[UpdateStatusFormSlack]):
|
|
|
133
136
|
)
|
|
134
137
|
return
|
|
135
138
|
|
|
139
|
+
# All validations passed, acknowledge the submission
|
|
140
|
+
ack()
|
|
141
|
+
|
|
136
142
|
update_kwargs: dict[str, Any] = {}
|
|
137
143
|
for changed_key in form.changed_data:
|
|
138
144
|
if changed_key in {"incident_category", "priority"}:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: firefighter-incident
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.23
|
|
4
4
|
Summary: Incident Management tool made for Slack using Django
|
|
5
5
|
Project-URL: Repository, https://github.com/ManoManoTech/firefighter-incident
|
|
6
6
|
Project-URL: Documentation, https://manomanotech.github.io/firefighter-incident/latest/
|