firefighter-incident 0.0.13__py3-none-any.whl → 0.0.15__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 +16 -3
- firefighter/api/serializers.py +17 -8
- firefighter/api/urls.py +8 -1
- firefighter/api/views/_base.py +1 -1
- firefighter/api/views/components.py +5 -5
- firefighter/api/views/incidents.py +9 -9
- firefighter/confluence/signals/incident_updated.py +2 -2
- firefighter/firefighter/settings/components/raid.py +3 -0
- firefighter/incidents/admin.py +24 -24
- firefighter/incidents/enums.py +22 -2
- firefighter/incidents/factories.py +14 -5
- firefighter/incidents/forms/close_incident.py +4 -4
- firefighter/incidents/forms/closure_reason.py +45 -0
- firefighter/incidents/forms/create_incident.py +4 -4
- firefighter/incidents/forms/unified_incident.py +406 -0
- firefighter/incidents/forms/update_status.py +91 -5
- firefighter/incidents/menus.py +2 -2
- firefighter/incidents/migrations/0005_enable_from_p1_to_p5_priority.py +7 -5
- firefighter/incidents/migrations/0009_update_sla.py +7 -5
- firefighter/incidents/migrations/0020_create_incident_category_model.py +64 -0
- firefighter/incidents/migrations/0021_copy_component_data_to_incident_category.py +57 -0
- firefighter/incidents/migrations/0022_add_incident_category_fields.py +34 -0
- firefighter/incidents/migrations/0023_populate_incident_category_references.py +57 -0
- firefighter/incidents/migrations/0024_remove_component_fields_and_model.py +26 -0
- firefighter/incidents/migrations/0025_make_incident_category_required.py +24 -0
- firefighter/incidents/migrations/0026_alter_incidentcategory_options_and_more.py +39 -0
- firefighter/incidents/migrations/0027_add_closure_fields.py +40 -0
- firefighter/incidents/migrations/0028_add_closure_reason_constraint.py +33 -0
- firefighter/incidents/migrations/0029_add_custom_fields_to_incident.py +22 -0
- firefighter/incidents/models/__init__.py +1 -1
- firefighter/incidents/models/group.py +1 -1
- firefighter/incidents/models/incident.py +47 -20
- firefighter/incidents/models/{component.py → incident_category.py} +30 -29
- firefighter/incidents/models/incident_update.py +3 -3
- firefighter/incidents/static/css/main.min.css +1 -1
- firefighter/incidents/tables.py +9 -9
- firefighter/incidents/templates/layouts/partials/incident_card.html +1 -1
- firefighter/incidents/templates/layouts/partials/incident_timeline.html +2 -2
- firefighter/incidents/templates/layouts/partials/status_pill.html +1 -1
- firefighter/incidents/templates/pages/{component_detail.html → incident_category_detail.html} +13 -13
- firefighter/incidents/templates/pages/{component_list.html → incident_category_list.html} +2 -2
- firefighter/incidents/templates/pages/incident_detail.html +3 -3
- firefighter/incidents/urls.py +6 -6
- firefighter/incidents/views/components/details.py +9 -9
- firefighter/incidents/views/components/list.py +9 -9
- firefighter/incidents/views/reports.py +5 -5
- firefighter/incidents/views/users/details.py +2 -2
- firefighter/incidents/views/views.py +7 -7
- firefighter/jira_app/client.py +1 -1
- firefighter/logging/custom_json_formatter.py +2 -1
- firefighter/pagerduty/tasks/trigger_oncall.py +1 -1
- firefighter/raid/admin.py +0 -11
- firefighter/raid/apps.py +9 -26
- firefighter/raid/client.py +5 -5
- firefighter/raid/forms.py +84 -213
- firefighter/raid/migrations/0003_delete_raidarea.py +16 -0
- firefighter/raid/models.py +2 -21
- firefighter/raid/serializers.py +5 -4
- firefighter/raid/service.py +29 -27
- firefighter/raid/signals/incident_created.py +42 -15
- firefighter/raid/signals/incident_updated.py +3 -2
- firefighter/raid/utils.py +1 -1
- firefighter/raid/views/__init__.py +1 -1
- firefighter/slack/admin.py +8 -8
- firefighter/slack/management/commands/switch_test_users.py +272 -0
- firefighter/slack/messages/slack_messages.py +24 -9
- firefighter/slack/migrations/0005_add_incident_categories_fields.py +33 -0
- firefighter/slack/migrations/0006_copy_components_to_incident_categories.py +57 -0
- firefighter/slack/migrations/0007_remove_components_fields.py +22 -0
- firefighter/slack/migrations/0008_alter_conversation_incident_categories_and_more.py +33 -0
- firefighter/slack/models/conversation.py +3 -3
- firefighter/slack/models/incident_channel.py +1 -1
- firefighter/slack/models/user.py +1 -1
- firefighter/slack/models/user_group.py +3 -3
- firefighter/slack/rules.py +2 -2
- firefighter/slack/signals/create_incident_conversation.py +6 -0
- firefighter/slack/signals/get_users.py +2 -2
- firefighter/slack/signals/incident_updated.py +8 -2
- firefighter/slack/utils.py +2 -2
- firefighter/slack/views/events/home.py +2 -2
- firefighter/slack/views/modals/__init__.py +4 -0
- firefighter/slack/views/modals/base_modal/form_utils.py +78 -0
- firefighter/slack/views/modals/close.py +18 -5
- firefighter/slack/views/modals/closure_reason.py +193 -0
- firefighter/slack/views/modals/open.py +83 -12
- firefighter/slack/views/modals/opening/check_current_incidents.py +2 -2
- firefighter/slack/views/modals/opening/details/unified.py +203 -0
- firefighter/slack/views/modals/opening/select_impact.py +5 -2
- firefighter/slack/views/modals/opening/set_details.py +3 -2
- firefighter/slack/views/modals/postmortem.py +10 -2
- firefighter/slack/views/modals/update_status.py +32 -6
- firefighter/slack/views/modals/utils.py +51 -0
- firefighter_fixtures/incidents/{components.json → incident_categories.json} +52 -52
- {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/METADATA +2 -2
- {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/RECORD +133 -88
- firefighter_tests/conftest.py +4 -5
- firefighter_tests/test_api/test_api_landbot.py +1 -1
- firefighter_tests/test_firefighter/test_sso.py +146 -0
- firefighter_tests/test_incidents/test_enums.py +100 -0
- firefighter_tests/test_incidents/test_forms/conftest.py +179 -0
- firefighter_tests/test_incidents/test_forms/test_closure_reason.py +91 -0
- firefighter_tests/test_incidents/test_forms/test_form_utils.py +15 -15
- firefighter_tests/test_incidents/test_forms/test_unified_incident_form.py +570 -0
- firefighter_tests/test_incidents/test_forms/test_unified_incident_form_integration.py +581 -0
- firefighter_tests/test_incidents/test_forms/test_unified_incident_form_p4_p5.py +410 -0
- firefighter_tests/test_incidents/test_forms/test_update_status_workflow.py +343 -0
- firefighter_tests/test_incidents/test_forms/test_workflow_transitions.py +167 -0
- firefighter_tests/test_incidents/test_incident_urls.py +3 -3
- firefighter_tests/test_incidents/test_models/test_incident_category.py +165 -0
- firefighter_tests/test_incidents/test_models/test_incident_model.py +70 -2
- firefighter_tests/test_raid/conftest.py +154 -0
- firefighter_tests/test_raid/test_p1_p3_jira_fields.py +372 -0
- firefighter_tests/test_raid/test_priority_mapping.py +267 -0
- firefighter_tests/test_raid/test_raid_client.py +580 -0
- firefighter_tests/test_raid/test_raid_forms.py +552 -0
- firefighter_tests/test_raid/test_raid_models.py +185 -0
- firefighter_tests/test_raid/test_raid_serializers.py +507 -0
- firefighter_tests/test_raid/test_raid_service.py +442 -0
- firefighter_tests/test_raid/test_raid_signals.py +187 -0
- firefighter_tests/test_raid/test_raid_views.py +196 -0
- firefighter_tests/test_slack/messages/__init__.py +0 -0
- firefighter_tests/test_slack/messages/test_slack_messages.py +367 -0
- firefighter_tests/test_slack/views/modals/conftest.py +140 -0
- firefighter_tests/test_slack/views/modals/test_close.py +71 -9
- firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +138 -0
- firefighter_tests/test_slack/views/modals/test_form_utils_multiple_choice.py +249 -0
- firefighter_tests/test_slack/views/modals/test_open.py +146 -2
- firefighter_tests/test_slack/views/modals/test_opening_unified.py +421 -0
- firefighter_tests/test_slack/views/modals/test_update_status.py +331 -7
- firefighter_tests/test_slack/views/modals/test_utils.py +135 -0
- firefighter/raid/views/open_normal.py +0 -139
- firefighter/slack/views/modals/opening/details/critical.py +0 -88
- firefighter_fixtures/raid/area.json +0 -1
- {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/WHEEL +0 -0
- {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/entry_points.txt +0 -0
- {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from django import forms
|
|
7
|
+
from django.conf import settings
|
|
8
|
+
from slack_sdk.models.blocks.basic_components import MarkdownTextObject
|
|
9
|
+
from slack_sdk.models.blocks.blocks import ContextBlock, SectionBlock
|
|
10
|
+
|
|
11
|
+
from firefighter.incidents.forms.unified_incident import UnifiedIncidentForm
|
|
12
|
+
from firefighter.slack.views.modals.base_modal.form_utils import SlackForm
|
|
13
|
+
from firefighter.slack.views.modals.opening.set_details import SetIncidentDetails
|
|
14
|
+
from firefighter.slack.views.modals.opening.types import OpeningData
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from firefighter.slack.views.modals.base_modal.form_utils import (
|
|
18
|
+
SlackFormAttributesDict,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UnifiedIncidentFormSlack(UnifiedIncidentForm):
|
|
25
|
+
"""Slack version of UnifiedIncidentForm with Slack-specific field configurations."""
|
|
26
|
+
|
|
27
|
+
slack_fields: SlackFormAttributesDict = {
|
|
28
|
+
"title": {
|
|
29
|
+
"input": {
|
|
30
|
+
"multiline": False,
|
|
31
|
+
"placeholder": "Short, punchy description of what's happening.",
|
|
32
|
+
},
|
|
33
|
+
"block": {"hint": None},
|
|
34
|
+
},
|
|
35
|
+
"description": {
|
|
36
|
+
"input": {
|
|
37
|
+
"multiline": True,
|
|
38
|
+
"placeholder": "Help people responding to the incident. This will be posted to #tech-incidents and on our internal status page.\nThis description can be edited later.",
|
|
39
|
+
},
|
|
40
|
+
"block": {"hint": None},
|
|
41
|
+
},
|
|
42
|
+
"environment": {
|
|
43
|
+
"input": {
|
|
44
|
+
"placeholder": "Select environments",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
"platform": {
|
|
48
|
+
"input": {
|
|
49
|
+
"placeholder": "Select platforms",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
"priority": {
|
|
53
|
+
"input": {
|
|
54
|
+
"placeholder": "Select a priority",
|
|
55
|
+
},
|
|
56
|
+
"widget": {
|
|
57
|
+
"post_block": (
|
|
58
|
+
SectionBlock(
|
|
59
|
+
text=f"_<{settings.SLACK_SEVERITY_HELP_GUIDE_URL}|How to choose the priority?>_"
|
|
60
|
+
)
|
|
61
|
+
if settings.SLACK_SEVERITY_HELP_GUIDE_URL
|
|
62
|
+
else None
|
|
63
|
+
),
|
|
64
|
+
"label_from_instance": lambda obj: f"{obj.emoji} {obj.name} - {obj.description}",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
"incident_category": {
|
|
68
|
+
"input": {
|
|
69
|
+
"placeholder": "Select affected issue category",
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
"suggested_team_routing": {
|
|
73
|
+
"input": {
|
|
74
|
+
"placeholder": "Select feature team or train",
|
|
75
|
+
},
|
|
76
|
+
"widget": {
|
|
77
|
+
"post_block": ContextBlock(
|
|
78
|
+
elements=[
|
|
79
|
+
MarkdownTextObject(
|
|
80
|
+
text="Feature Team or Train that should own the issue. If you don't know access <https://manomano.atlassian.net/wiki/spaces/QRAFT/pages/3970335291/Teams+and+owners|here> for guidance."
|
|
81
|
+
),
|
|
82
|
+
]
|
|
83
|
+
)
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
"zendesk_ticket_id": {
|
|
87
|
+
"input": {
|
|
88
|
+
"placeholder": "Zendesk ticket ID (if applicable)",
|
|
89
|
+
},
|
|
90
|
+
"block": {
|
|
91
|
+
"hint": "Link this incident to an existing Zendesk customer ticket"
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
"seller_contract_id": {
|
|
95
|
+
"input": {
|
|
96
|
+
"placeholder": "Seller contract ID",
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
"zoho_desk_ticket_id": {
|
|
100
|
+
"input": {
|
|
101
|
+
"placeholder": "Zoho Desk ticket ID (if applicable)",
|
|
102
|
+
},
|
|
103
|
+
"block": {
|
|
104
|
+
"hint": "Link this incident to an existing Zoho Desk seller ticket"
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
def __init__(
|
|
110
|
+
self,
|
|
111
|
+
*args: Any,
|
|
112
|
+
impacts_data: dict[str, Any] | None = None,
|
|
113
|
+
response_type: str = "critical",
|
|
114
|
+
**kwargs: Any,
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Initialize form with impact-based field visibility.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
*args: Positional arguments passed to parent form
|
|
120
|
+
impacts_data: Dictionary of impact selections
|
|
121
|
+
response_type: "critical" or "normal"
|
|
122
|
+
**kwargs: Keyword arguments passed to parent form
|
|
123
|
+
"""
|
|
124
|
+
super().__init__(*args, **kwargs)
|
|
125
|
+
|
|
126
|
+
# Store for later use
|
|
127
|
+
self._impacts_data = impacts_data or {}
|
|
128
|
+
self._response_type = response_type
|
|
129
|
+
|
|
130
|
+
# Make priority field hidden
|
|
131
|
+
self.fields["priority"].widget = forms.HiddenInput()
|
|
132
|
+
|
|
133
|
+
# Conditionally hide fields based on impacts and response_type
|
|
134
|
+
self._configure_field_visibility()
|
|
135
|
+
|
|
136
|
+
def _configure_field_visibility(self) -> None:
|
|
137
|
+
"""Configure which fields should be visible based on impacts and response type."""
|
|
138
|
+
visible_fields = self.get_visible_fields_for_impacts(
|
|
139
|
+
self._impacts_data, self._response_type
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Remove fields that shouldn't be visible
|
|
143
|
+
fields_to_remove = [
|
|
144
|
+
field_name for field_name in self.fields if field_name not in visible_fields
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
for field_name in fields_to_remove:
|
|
148
|
+
del self.fields[field_name]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class OpeningUnifiedModal(SetIncidentDetails[UnifiedIncidentFormSlack]):
|
|
152
|
+
"""Unified modal for all incident types (P1-P5)."""
|
|
153
|
+
|
|
154
|
+
open_action: str = "open_incident_unified"
|
|
155
|
+
push_action: str = "push_incident_unified"
|
|
156
|
+
callback_id: str = "open_incident_unified"
|
|
157
|
+
id = "incident_unified"
|
|
158
|
+
|
|
159
|
+
title = "Incident Details"
|
|
160
|
+
form_class = UnifiedIncidentFormSlack
|
|
161
|
+
|
|
162
|
+
def get_form_class(self) -> Any:
|
|
163
|
+
"""Return a SlackForm wrapper that passes impacts_data and response_type."""
|
|
164
|
+
form_class = self.form_class
|
|
165
|
+
|
|
166
|
+
# Create a custom form class that accepts our parameters
|
|
167
|
+
class ContextAwareForm(form_class): # type: ignore
|
|
168
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
169
|
+
# Extract context from kwargs
|
|
170
|
+
open_incident_context = kwargs.pop("open_incident_context", None)
|
|
171
|
+
if open_incident_context:
|
|
172
|
+
kwargs["impacts_data"] = open_incident_context.get("impact_form_data", {})
|
|
173
|
+
kwargs["response_type"] = open_incident_context.get("response_type", "critical")
|
|
174
|
+
super().__init__(*args, **kwargs)
|
|
175
|
+
|
|
176
|
+
# Return SlackForm wrapping the context-aware form
|
|
177
|
+
return SlackForm(ContextAwareForm)
|
|
178
|
+
|
|
179
|
+
def build_modal_fn(
|
|
180
|
+
self, open_incident_context: OpeningData | None = None, **kwargs: Any
|
|
181
|
+
) -> Any:
|
|
182
|
+
"""Build modal with impact-aware form."""
|
|
183
|
+
# Extract impacts and response type from context
|
|
184
|
+
if open_incident_context is None:
|
|
185
|
+
open_incident_context = OpeningData()
|
|
186
|
+
|
|
187
|
+
response_type = open_incident_context.get("response_type", "critical")
|
|
188
|
+
|
|
189
|
+
# Store in initial data so form can access it
|
|
190
|
+
details_form_data = open_incident_context.get("details_form_data")
|
|
191
|
+
if details_form_data is None:
|
|
192
|
+
details_form_data = {}
|
|
193
|
+
details_form_data["response_type"] = response_type
|
|
194
|
+
|
|
195
|
+
# Update context
|
|
196
|
+
open_incident_context["details_form_data"] = details_form_data
|
|
197
|
+
|
|
198
|
+
# Store context for get_form_class in kwargs
|
|
199
|
+
# Call parent build_modal_fn with open_incident_context in kwargs
|
|
200
|
+
return super().build_modal_fn(open_incident_context=open_incident_context, **kwargs)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
modal_opening_unified = OpeningUnifiedModal()
|
|
@@ -139,13 +139,16 @@ class SelectImpactModal(
|
|
|
139
139
|
impact_descriptions = ""
|
|
140
140
|
if impact_form_data:
|
|
141
141
|
for value in impact_form_data.values():
|
|
142
|
-
|
|
142
|
+
# Filter out "no impact" levels using the value field instead of name
|
|
143
|
+
if not ((hasattr(value, "value") and value.value == "NO") or value.name == "NO" or not value.description):
|
|
143
144
|
if hasattr(value, "impact_type_id") and value.impact_type_id:
|
|
144
145
|
impact_type = ImpactType.objects.get(pk=value.impact_type_id)
|
|
145
146
|
if impact_type:
|
|
146
147
|
impact_descriptions += f"\u00A0\u00A0 :exclamation: {impact_type} - {value}\n"
|
|
147
148
|
for line in str(value.description).splitlines():
|
|
148
|
-
|
|
149
|
+
# Skip empty lines or lines with only dashes/whitespace
|
|
150
|
+
if line.strip() and line.strip() != "-":
|
|
151
|
+
impact_descriptions += f"\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0 • {line}\n"
|
|
149
152
|
return impact_descriptions
|
|
150
153
|
|
|
151
154
|
def handle_modal_fn( # type: ignore
|
|
@@ -78,7 +78,7 @@ class SetIncidentDetails(ModalForm[T], Generic[T]):
|
|
|
78
78
|
submit=self.submit_text[:24],
|
|
79
79
|
close="Close details",
|
|
80
80
|
callback_id=self.callback_id,
|
|
81
|
-
blocks=self.get_form_class()(initial=details_form_data).slack_blocks(),
|
|
81
|
+
blocks=self.get_form_class()(initial=details_form_data, open_incident_context=open_incident_context, **kwargs).slack_blocks(),
|
|
82
82
|
private_metadata=json.dumps(
|
|
83
83
|
open_incident_context, cls=SlackFormJSONEncoder
|
|
84
84
|
),
|
|
@@ -99,7 +99,8 @@ class SetIncidentDetails(ModalForm[T], Generic[T]):
|
|
|
99
99
|
priority = Priority.objects.get(pk=priority)
|
|
100
100
|
|
|
101
101
|
slack_form = self.get_form_class()(
|
|
102
|
-
data={**slack_view_submission_to_dict(body), "priority": priority}
|
|
102
|
+
data={**slack_view_submission_to_dict(body), "priority": priority},
|
|
103
|
+
open_incident_context=private_metadata,
|
|
103
104
|
)
|
|
104
105
|
form: T = slack_form.form
|
|
105
106
|
if form.is_valid():
|
|
@@ -65,14 +65,22 @@ class PostMortemModal(
|
|
|
65
65
|
)
|
|
66
66
|
|
|
67
67
|
@staticmethod
|
|
68
|
-
def handle_modal_fn(ack: Ack, incident: Incident) -> None: # type: ignore[override]
|
|
68
|
+
def handle_modal_fn(ack: Ack, body: dict[str, Any], incident: Incident) -> None: # type: ignore[override]
|
|
69
69
|
if not apps.is_installed("firefighter.confluence"):
|
|
70
70
|
ack(text="Confluence is not enabled!")
|
|
71
71
|
return
|
|
72
72
|
if hasattr(incident, "postmortem_for"):
|
|
73
73
|
ack(text="Post-mortem has already been created.")
|
|
74
74
|
return
|
|
75
|
-
|
|
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
|
+
|
|
76
84
|
PostMortem.objects.create_postmortem_for_incident(incident)
|
|
77
85
|
|
|
78
86
|
|
|
@@ -7,9 +7,11 @@ from django.conf import settings
|
|
|
7
7
|
from slack_sdk.models.blocks.blocks import SectionBlock
|
|
8
8
|
from slack_sdk.models.views import View
|
|
9
9
|
|
|
10
|
+
from firefighter.incidents.enums import IncidentStatus
|
|
10
11
|
from firefighter.incidents.forms.update_status import UpdateStatusForm
|
|
11
12
|
from firefighter.slack.slack_templating import slack_block_footer, slack_block_separator
|
|
12
13
|
from firefighter.slack.views.modals.base_modal.base import ModalForm
|
|
14
|
+
from firefighter.slack.views.modals.utils import handle_update_status_close_request
|
|
13
15
|
|
|
14
16
|
if TYPE_CHECKING:
|
|
15
17
|
from slack_bolt.context.ack.ack import Ack
|
|
@@ -51,7 +53,7 @@ class UpdateStatusFormSlack(UpdateStatusForm):
|
|
|
51
53
|
"label_from_instance": priority_label,
|
|
52
54
|
},
|
|
53
55
|
},
|
|
54
|
-
"
|
|
56
|
+
"incident_category": {
|
|
55
57
|
"input": {
|
|
56
58
|
"placeholder": "Select affected issue category",
|
|
57
59
|
}
|
|
@@ -73,8 +75,9 @@ class UpdateStatusModal(ModalForm[UpdateStatusFormSlack]):
|
|
|
73
75
|
initial={
|
|
74
76
|
"status": incident.status,
|
|
75
77
|
"priority": incident.priority,
|
|
76
|
-
"
|
|
77
|
-
}
|
|
78
|
+
"incident_category": incident.incident_category,
|
|
79
|
+
},
|
|
80
|
+
incident=incident,
|
|
78
81
|
).slack_blocks()
|
|
79
82
|
blocks.append(slack_block_separator())
|
|
80
83
|
blocks.append(slack_block_footer())
|
|
@@ -97,8 +100,9 @@ class UpdateStatusModal(ModalForm[UpdateStatusFormSlack]):
|
|
|
97
100
|
"initial": {
|
|
98
101
|
"status": incident.status,
|
|
99
102
|
"priority": incident.priority,
|
|
100
|
-
"
|
|
101
|
-
}
|
|
103
|
+
"incident_category": incident.incident_category,
|
|
104
|
+
},
|
|
105
|
+
"incident": incident,
|
|
102
106
|
},
|
|
103
107
|
)
|
|
104
108
|
if slack_form is None:
|
|
@@ -107,9 +111,31 @@ class UpdateStatusModal(ModalForm[UpdateStatusFormSlack]):
|
|
|
107
111
|
if len(form.cleaned_data) == 0:
|
|
108
112
|
# XXX We should have a prompt for empty forms
|
|
109
113
|
return
|
|
114
|
+
|
|
115
|
+
# Check if user is trying to close and needs a closure reason
|
|
116
|
+
if "status" in form.changed_data:
|
|
117
|
+
target_status = form.cleaned_data["status"]
|
|
118
|
+
if handle_update_status_close_request(ack, body, incident, target_status):
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
# If trying to close, validate that incident can be closed
|
|
122
|
+
if target_status == IncidentStatus.CLOSED:
|
|
123
|
+
can_close, reasons = incident.can_be_closed
|
|
124
|
+
if not can_close:
|
|
125
|
+
# Build error message from reasons
|
|
126
|
+
error_messages = [reason[1] for reason in reasons]
|
|
127
|
+
error_text = "\n".join([f"• {msg}" for msg in error_messages])
|
|
128
|
+
ack(
|
|
129
|
+
response_action="errors",
|
|
130
|
+
errors={
|
|
131
|
+
"status": f"Cannot close this incident:\n{error_text}"
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
return
|
|
135
|
+
|
|
110
136
|
update_kwargs: dict[str, Any] = {}
|
|
111
137
|
for changed_key in form.changed_data:
|
|
112
|
-
if changed_key in {"
|
|
138
|
+
if changed_key in {"incident_category", "priority"}:
|
|
113
139
|
update_kwargs[f"{changed_key}_id"] = form.cleaned_data[changed_key].id
|
|
114
140
|
if changed_key in {"description", "title", "message", "status"}:
|
|
115
141
|
update_kwargs[changed_key] = form.cleaned_data[changed_key]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Utilities for modal handling to avoid circular imports."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from firefighter.incidents.enums import IncidentStatus
|
|
7
|
+
from firefighter.incidents.forms.update_status import UpdateStatusForm
|
|
8
|
+
from firefighter.slack.views.modals.closure_reason import modal_closure_reason
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from slack_sdk.models.views import View
|
|
12
|
+
|
|
13
|
+
from firefighter.incidents.models.incident import Incident
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_close_modal_view(body: dict[str, Any], incident: Incident, **kwargs: Any) -> View | None:
|
|
17
|
+
"""Get the appropriate modal view for closing an incident.
|
|
18
|
+
|
|
19
|
+
This function determines whether to show the closure reason modal
|
|
20
|
+
or delegate to the normal close modal.
|
|
21
|
+
"""
|
|
22
|
+
# Check if closure reason is required
|
|
23
|
+
if UpdateStatusForm.requires_closure_reason(incident, IncidentStatus.CLOSED):
|
|
24
|
+
return modal_closure_reason.build_modal_fn(body, incident, **kwargs)
|
|
25
|
+
|
|
26
|
+
# Return None to indicate normal close modal should be used
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def handle_close_modal_callback(ack: Any, body: dict[str, Any], incident: Incident, user: Any) -> bool | None:
|
|
31
|
+
"""Handle modal callback, delegating to closure reason modal if needed."""
|
|
32
|
+
# Check if this is a closure reason modal callback
|
|
33
|
+
if body.get("view", {}).get("callback_id") == "incident_closure_reason":
|
|
34
|
+
return modal_closure_reason.handle_modal_fn(ack, body, incident, user)
|
|
35
|
+
|
|
36
|
+
# Return None to indicate normal handling should continue
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def handle_update_status_close_request(ack: Any, body: dict[str, Any], incident: Incident, target_status: IncidentStatus) -> bool:
|
|
41
|
+
"""Handle update status request to close incident, showing reason modal if needed.
|
|
42
|
+
|
|
43
|
+
Returns True if the request was handled (reason modal shown), False otherwise.
|
|
44
|
+
"""
|
|
45
|
+
if (target_status == IncidentStatus.CLOSED and
|
|
46
|
+
UpdateStatusForm.requires_closure_reason(incident, target_status)):
|
|
47
|
+
# Show closure reason modal instead
|
|
48
|
+
ack(response_action="push", view=modal_closure_reason.build_modal_fn(body, incident))
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
return False
|