firefighter-incident 0.0.14__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 +2 -2
- firefighter/api/serializers.py +9 -0
- firefighter/confluence/signals/incident_updated.py +2 -2
- firefighter/incidents/enums.py +22 -2
- firefighter/incidents/forms/closure_reason.py +45 -0
- firefighter/incidents/forms/unified_incident.py +406 -0
- firefighter/incidents/forms/update_status.py +87 -1
- 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/incident.py +32 -5
- firefighter/incidents/static/css/main.min.css +1 -1
- firefighter/incidents/templates/layouts/partials/status_pill.html +1 -1
- firefighter/incidents/views/reports.py +3 -3
- firefighter/raid/apps.py +9 -26
- firefighter/raid/client.py +2 -2
- firefighter/raid/forms.py +75 -238
- firefighter/raid/signals/incident_created.py +38 -13
- firefighter/raid/signals/incident_updated.py +3 -2
- firefighter/slack/messages/slack_messages.py +19 -4
- firefighter/slack/rules.py +1 -1
- firefighter/slack/signals/create_incident_conversation.py +6 -0
- firefighter/slack/signals/incident_updated.py +7 -1
- firefighter/slack/views/modals/__init__.py +4 -0
- firefighter/slack/views/modals/base_modal/form_utils.py +63 -0
- firefighter/slack/views/modals/close.py +15 -2
- firefighter/slack/views/modals/closure_reason.py +193 -0
- firefighter/slack/views/modals/open.py +59 -12
- firefighter/slack/views/modals/opening/details/unified.py +203 -0
- 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 +28 -2
- firefighter/slack/views/modals/utils.py +51 -0
- {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.15.dist-info}/METADATA +1 -1
- {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.15.dist-info}/RECORD +61 -37
- 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_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_models/test_incident_model.py +68 -0
- 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_raid_forms.py +10 -253
- firefighter_tests/test_raid/test_raid_signals.py +187 -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 +65 -3
- 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 +327 -3
- 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_incident-0.0.14.dist-info → firefighter_incident-0.0.15.dist-info}/WHEEL +0 -0
- {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.15.dist-info}/entry_points.txt +0 -0
- {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.15.dist-info}/licenses/LICENSE +0 -0
firefighter/raid/apps.py
CHANGED
|
@@ -20,35 +20,18 @@ class RaidConfig(AppConfig):
|
|
|
20
20
|
incident_created,
|
|
21
21
|
incident_updated,
|
|
22
22
|
)
|
|
23
|
-
from firefighter.raid.views.open_normal import (
|
|
24
|
-
OpeningRaidCustomerModal,
|
|
25
|
-
OpeningRaidDocumentationRequestModal,
|
|
26
|
-
OpeningRaidFeatureRequestModal,
|
|
27
|
-
OpeningRaidInternalModal,
|
|
28
|
-
OpeningRaidSellerModal,
|
|
29
|
-
)
|
|
30
23
|
from firefighter.slack.views.modals.open import INCIDENT_TYPES
|
|
24
|
+
from firefighter.slack.views.modals.opening.details.unified import (
|
|
25
|
+
OpeningUnifiedModal,
|
|
26
|
+
)
|
|
31
27
|
|
|
28
|
+
# Use unified form for all normal incidents (P4-P5)
|
|
29
|
+
# This replaces the previous 5 separate forms (Customer/Seller/Internal/Doc/Feature)
|
|
30
|
+
# STEP 3 (incident type selection) will be automatically hidden since len() == 1
|
|
32
31
|
INCIDENT_TYPES["normal"] = {
|
|
33
|
-
"
|
|
34
|
-
"label": "
|
|
35
|
-
"slack_form":
|
|
36
|
-
},
|
|
37
|
-
"SELLER": {
|
|
38
|
-
"label": "Seller",
|
|
39
|
-
"slack_form": OpeningRaidSellerModal,
|
|
40
|
-
},
|
|
41
|
-
"INTERNAL": {
|
|
42
|
-
"label": "Internal",
|
|
43
|
-
"slack_form": OpeningRaidInternalModal,
|
|
44
|
-
},
|
|
45
|
-
"DOCUMENTATION_REQUEST": {
|
|
46
|
-
"label": "Documentation request",
|
|
47
|
-
"slack_form": OpeningRaidDocumentationRequestModal,
|
|
48
|
-
},
|
|
49
|
-
"FEATURE_REQUEST": {
|
|
50
|
-
"label": "Feature request",
|
|
51
|
-
"slack_form": OpeningRaidFeatureRequestModal,
|
|
32
|
+
"normal": {
|
|
33
|
+
"label": "Normal",
|
|
34
|
+
"slack_form": OpeningUnifiedModal,
|
|
52
35
|
},
|
|
53
36
|
}
|
|
54
37
|
|
firefighter/raid/client.py
CHANGED
|
@@ -77,9 +77,9 @@ class RaidJiraClient(JiraClient):
|
|
|
77
77
|
f"Seller link to TOOLBOX: {TOOLBOX_URL}?seller_id={seller_contract_id}"
|
|
78
78
|
)
|
|
79
79
|
extra_args["customfield_10908"] = str(seller_contract_id)
|
|
80
|
-
if is_seller_in_golden_list:
|
|
80
|
+
if is_seller_in_golden_list is True:
|
|
81
81
|
labels = [*labels, "goldenList"]
|
|
82
|
-
if is_key_account:
|
|
82
|
+
if is_key_account is True:
|
|
83
83
|
labels = [*labels, "keyAccount"]
|
|
84
84
|
if suggested_team_routing and suggested_team_routing != "SBI":
|
|
85
85
|
description_addendum.append(
|
firefighter/raid/forms.py
CHANGED
|
@@ -3,15 +3,11 @@ from __future__ import annotations
|
|
|
3
3
|
import logging
|
|
4
4
|
from typing import TYPE_CHECKING, Any, Never
|
|
5
5
|
|
|
6
|
-
from django import forms
|
|
7
6
|
from django.conf import settings
|
|
8
7
|
from django.db import models
|
|
9
8
|
from slack_sdk.errors import SlackApiError
|
|
10
9
|
|
|
11
|
-
from firefighter.incidents.forms.create_incident import CreateIncidentFormBase
|
|
12
10
|
from firefighter.incidents.forms.select_impact import SelectImpactForm
|
|
13
|
-
from firefighter.incidents.forms.utils import GroupedModelChoiceField
|
|
14
|
-
from firefighter.incidents.models import IncidentCategory
|
|
15
11
|
from firefighter.incidents.models.priority import Priority
|
|
16
12
|
from firefighter.jira_app.client import (
|
|
17
13
|
JiraAPIError,
|
|
@@ -23,16 +19,7 @@ from firefighter.raid.messages import (
|
|
|
23
19
|
SlackMessageRaidCreatedIssue,
|
|
24
20
|
SlackMessageRaidModifiedIssue,
|
|
25
21
|
)
|
|
26
|
-
from firefighter.raid.models import
|
|
27
|
-
from firefighter.raid.service import (
|
|
28
|
-
CustomerIssueData,
|
|
29
|
-
create_issue_customer,
|
|
30
|
-
create_issue_documentation_request,
|
|
31
|
-
create_issue_feature_request,
|
|
32
|
-
create_issue_internal,
|
|
33
|
-
create_issue_seller,
|
|
34
|
-
get_jira_user_from_user,
|
|
35
|
-
)
|
|
22
|
+
from firefighter.raid.models import JiraTicket
|
|
36
23
|
from firefighter.raid.utils import get_domain_from_email
|
|
37
24
|
from firefighter.slack.models.conversation import Conversation
|
|
38
25
|
|
|
@@ -63,230 +50,11 @@ def initial_priority() -> Priority:
|
|
|
63
50
|
return Priority.objects.get(default=True)
|
|
64
51
|
|
|
65
52
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
title = forms.CharField(
|
|
72
|
-
label="Title",
|
|
73
|
-
max_length=128,
|
|
74
|
-
min_length=10,
|
|
75
|
-
widget=forms.TextInput(attrs={"placeholder": "What's going on?"}),
|
|
76
|
-
)
|
|
77
|
-
description = forms.CharField(
|
|
78
|
-
label="Summary",
|
|
79
|
-
min_length=10,
|
|
80
|
-
max_length=1200,
|
|
81
|
-
)
|
|
82
|
-
suggested_team_routing = forms.ModelChoiceField(
|
|
83
|
-
queryset=FeatureTeam.objects.only("name").order_by("name"),
|
|
84
|
-
label="Feature Team or Train",
|
|
85
|
-
required=True,
|
|
86
|
-
)
|
|
87
|
-
priority = forms.ModelChoiceField(
|
|
88
|
-
label="Priority",
|
|
89
|
-
queryset=Priority.objects.filter(enabled_create=True),
|
|
90
|
-
initial=initial_priority,
|
|
91
|
-
widget=forms.HiddenInput(),
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
field_order = [
|
|
95
|
-
"incident_category",
|
|
96
|
-
"platform",
|
|
97
|
-
"title",
|
|
98
|
-
"description",
|
|
99
|
-
"seller_contract_id",
|
|
100
|
-
"is_key_account",
|
|
101
|
-
"is_seller_in_golden_list",
|
|
102
|
-
"zoho_desk_ticket_id",
|
|
103
|
-
"zendesk_ticket_id",
|
|
104
|
-
"suggested_team_routing",
|
|
105
|
-
]
|
|
106
|
-
|
|
107
|
-
def trigger_incident_workflow(
|
|
108
|
-
self,
|
|
109
|
-
creator: User,
|
|
110
|
-
impacts_data: dict[str, ImpactLevel],
|
|
111
|
-
*args: Any,
|
|
112
|
-
**kwargs: Any,
|
|
113
|
-
) -> Any:
|
|
114
|
-
raise NotImplementedError
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
class CreateNormalCustomerIncidentForm(CreateNormalIncidentFormBase):
|
|
118
|
-
incident_category = GroupedModelChoiceField(
|
|
119
|
-
choices_groupby="group",
|
|
120
|
-
queryset=IncidentCategory.objects.all().select_related("group").order_by("group__order", "name"),
|
|
121
|
-
label="Incident category"
|
|
122
|
-
)
|
|
123
|
-
zendesk_ticket_id = forms.CharField(
|
|
124
|
-
label="Zendesk Ticket ID", max_length=128, min_length=2, required=False
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
# XXX business impact: infer from impact/add in impact modal?
|
|
128
|
-
def trigger_incident_workflow(
|
|
129
|
-
self,
|
|
130
|
-
creator: User,
|
|
131
|
-
impacts_data: dict[str, ImpactLevel],
|
|
132
|
-
*args: Never,
|
|
133
|
-
**kwargs: Never,
|
|
134
|
-
) -> None:
|
|
135
|
-
jira_user: JiraUser = get_jira_user_from_user(creator)
|
|
136
|
-
customer_data = CustomerIssueData(
|
|
137
|
-
priority=self.cleaned_data["priority"].value,
|
|
138
|
-
labels=[""],
|
|
139
|
-
platform=self.cleaned_data["platform"],
|
|
140
|
-
business_impact=str(get_business_impact(impacts_data)),
|
|
141
|
-
team_to_be_routed=self.cleaned_data["suggested_team_routing"],
|
|
142
|
-
area=None,
|
|
143
|
-
zendesk_ticket_id=self.cleaned_data["zendesk_ticket_id"],
|
|
144
|
-
incident_category=self.cleaned_data["incident_category"].name,
|
|
145
|
-
)
|
|
146
|
-
issue_data = create_issue_customer(
|
|
147
|
-
title=self.cleaned_data["title"],
|
|
148
|
-
description=self.cleaned_data["description"],
|
|
149
|
-
reporter=jira_user.id,
|
|
150
|
-
issue_data=customer_data,
|
|
151
|
-
)
|
|
152
|
-
process_jira_issue(
|
|
153
|
-
issue_data, creator, jira_user=jira_user, impacts_data=impacts_data
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
class CreateRaidDocumentationRequestIncidentForm(CreateNormalIncidentFormBase):
|
|
158
|
-
incident_category = GroupedModelChoiceField(
|
|
159
|
-
choices_groupby="group",
|
|
160
|
-
queryset=IncidentCategory.objects.all().select_related("group").order_by("group__order", "name"),
|
|
161
|
-
label="Incident category"
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
def trigger_incident_workflow(
|
|
165
|
-
self,
|
|
166
|
-
creator: User,
|
|
167
|
-
impacts_data: dict[str, ImpactLevel],
|
|
168
|
-
*args: Never,
|
|
169
|
-
**kwargs: Never,
|
|
170
|
-
) -> None:
|
|
171
|
-
jira_user: JiraUser = get_jira_user_from_user(creator)
|
|
172
|
-
issue_data = create_issue_documentation_request(
|
|
173
|
-
title=self.cleaned_data["title"],
|
|
174
|
-
description=self.cleaned_data["description"],
|
|
175
|
-
priority=self.cleaned_data["priority"].value,
|
|
176
|
-
reporter=jira_user.id,
|
|
177
|
-
platform=self.cleaned_data["platform"],
|
|
178
|
-
labels=[""],
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
process_jira_issue(
|
|
182
|
-
issue_data, creator, jira_user=jira_user, impacts_data=impacts_data
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
class CreateRaidFeatureRequestIncidentForm(CreateNormalIncidentFormBase):
|
|
187
|
-
incident_category = GroupedModelChoiceField(
|
|
188
|
-
choices_groupby="group",
|
|
189
|
-
queryset=IncidentCategory.objects.all().select_related("group").order_by("group__order", "name"),
|
|
190
|
-
label="Incident category"
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
def trigger_incident_workflow(
|
|
194
|
-
self,
|
|
195
|
-
creator: User,
|
|
196
|
-
impacts_data: dict[str, ImpactLevel],
|
|
197
|
-
*args: Never,
|
|
198
|
-
**kwargs: Never,
|
|
199
|
-
) -> None:
|
|
200
|
-
jira_user: JiraUser = get_jira_user_from_user(creator)
|
|
201
|
-
issue_data = create_issue_feature_request(
|
|
202
|
-
title=self.cleaned_data["title"],
|
|
203
|
-
description=self.cleaned_data["description"],
|
|
204
|
-
priority=self.cleaned_data["priority"].value,
|
|
205
|
-
reporter=jira_user.id,
|
|
206
|
-
platform=self.cleaned_data["platform"],
|
|
207
|
-
labels=[""],
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
process_jira_issue(
|
|
211
|
-
issue_data, creator, jira_user=jira_user, impacts_data=impacts_data
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
class CreateRaidInternalIncidentForm(CreateNormalIncidentFormBase):
|
|
216
|
-
incident_category = GroupedModelChoiceField(
|
|
217
|
-
choices_groupby="group",
|
|
218
|
-
queryset=IncidentCategory.objects.all().select_related("group").order_by("group__order", "name"),
|
|
219
|
-
label="Incident category"
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
def trigger_incident_workflow(
|
|
223
|
-
self,
|
|
224
|
-
creator: User,
|
|
225
|
-
impacts_data: dict[str, ImpactLevel],
|
|
226
|
-
*args: Never,
|
|
227
|
-
**kwargs: Never,
|
|
228
|
-
) -> None:
|
|
229
|
-
jira_user: JiraUser = get_jira_user_from_user(creator)
|
|
230
|
-
issue_data = create_issue_internal(
|
|
231
|
-
title=self.cleaned_data["title"],
|
|
232
|
-
description=self.cleaned_data["description"],
|
|
233
|
-
priority=self.cleaned_data["priority"].value,
|
|
234
|
-
reporter=jira_user.id,
|
|
235
|
-
platform=self.cleaned_data["platform"],
|
|
236
|
-
business_impact=str(get_business_impact(impacts_data)),
|
|
237
|
-
team_to_be_routed=self.cleaned_data["suggested_team_routing"],
|
|
238
|
-
incident_category=self.cleaned_data["incident_category"].name,
|
|
239
|
-
labels=[""],
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
process_jira_issue(
|
|
243
|
-
issue_data, creator, jira_user=jira_user, impacts_data=impacts_data
|
|
244
|
-
)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
class RaidCreateIncidentSellerForm(CreateNormalIncidentFormBase):
|
|
248
|
-
incident_category = GroupedModelChoiceField(
|
|
249
|
-
choices_groupby="group",
|
|
250
|
-
queryset=IncidentCategory.objects.all().select_related("group").order_by("group__order", "name"),
|
|
251
|
-
label="Incident category"
|
|
252
|
-
)
|
|
253
|
-
seller_contract_id = forms.CharField(
|
|
254
|
-
label="Seller Contract ID", max_length=128, min_length=0
|
|
255
|
-
)
|
|
256
|
-
is_key_account = forms.BooleanField(label="Is it a Key Account?", required=False)
|
|
257
|
-
is_seller_in_golden_list = forms.BooleanField(
|
|
258
|
-
label="Is the seller in the Golden List?", required=False
|
|
259
|
-
)
|
|
260
|
-
zoho_desk_ticket_id = forms.CharField(
|
|
261
|
-
required=False, label="Zoho Desk Ticket ID", max_length=128, min_length=1
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
def trigger_incident_workflow(
|
|
265
|
-
self,
|
|
266
|
-
creator: User,
|
|
267
|
-
impacts_data: dict[str, ImpactLevel],
|
|
268
|
-
*args: Never,
|
|
269
|
-
**kwargs: Never,
|
|
270
|
-
) -> None:
|
|
271
|
-
jira_user: JiraUser = get_jira_user_from_user(creator)
|
|
272
|
-
issue_data = create_issue_seller(
|
|
273
|
-
title=self.cleaned_data["title"],
|
|
274
|
-
description=self.cleaned_data["description"],
|
|
275
|
-
priority=self.cleaned_data["priority"].value,
|
|
276
|
-
reporter=jira_user.id,
|
|
277
|
-
platform=self.cleaned_data["platform"],
|
|
278
|
-
business_impact=str(get_business_impact(impacts_data)),
|
|
279
|
-
team_to_be_routed=self.cleaned_data["suggested_team_routing"],
|
|
280
|
-
incident_category=self.cleaned_data["incident_category"].name,
|
|
281
|
-
zoho_desk_ticket_id=self.cleaned_data["zoho_desk_ticket_id"],
|
|
282
|
-
is_key_account=self.cleaned_data["is_key_account"],
|
|
283
|
-
is_seller_in_golden_list=self.cleaned_data["is_seller_in_golden_list"],
|
|
284
|
-
seller_contract_id=self.cleaned_data["seller_contract_id"],
|
|
285
|
-
labels=[""],
|
|
286
|
-
)
|
|
287
|
-
process_jira_issue(
|
|
288
|
-
issue_data, creator, jira_user=jira_user, impacts_data=impacts_data
|
|
289
|
-
)
|
|
53
|
+
# NOTE: Incident creation forms have been unified and moved to:
|
|
54
|
+
# firefighter.incidents.forms.unified_incident.UnifiedIncidentForm
|
|
55
|
+
# This handles all incident types (P1-P5) with dynamic field visibility.
|
|
56
|
+
# The following utility functions remain here as they are used by the unified form
|
|
57
|
+
# and by JIRA webhook handlers.
|
|
290
58
|
|
|
291
59
|
|
|
292
60
|
def process_jira_issue(
|
|
@@ -476,6 +244,75 @@ def get_business_impact(impacts_data: dict[str, ImpactLevel]) -> str | None:
|
|
|
476
244
|
return impact_form.business_impact_new
|
|
477
245
|
|
|
478
246
|
|
|
247
|
+
def prepare_jira_fields(
|
|
248
|
+
*,
|
|
249
|
+
title: str,
|
|
250
|
+
description: str,
|
|
251
|
+
priority: int,
|
|
252
|
+
reporter: str,
|
|
253
|
+
incident_category: str,
|
|
254
|
+
environments: list[str],
|
|
255
|
+
platforms: list[str],
|
|
256
|
+
impacts_data: dict[str, ImpactLevel],
|
|
257
|
+
optional_fields: dict[str, Any] | None = None,
|
|
258
|
+
) -> dict[str, Any]:
|
|
259
|
+
"""Prepare all fields for jira_client.create_issue().
|
|
260
|
+
|
|
261
|
+
This function centralizes Jira field preparation for both P1-P3 and P4-P5 incidents,
|
|
262
|
+
ensuring all custom fields are properly passed.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
title: Incident title
|
|
266
|
+
description: Incident description
|
|
267
|
+
priority: Priority value (1-5)
|
|
268
|
+
reporter: Jira user account ID
|
|
269
|
+
incident_category: Category name
|
|
270
|
+
environments: List of environment values (e.g. ["PRD", "STG"])
|
|
271
|
+
platforms: List of platform values (e.g. ["platform-FR", "platform-DE"])
|
|
272
|
+
impacts_data: Dictionary of impact data for business_impact computation
|
|
273
|
+
optional_fields: Optional dictionary containing:
|
|
274
|
+
- zendesk_ticket_id: Zendesk ticket ID (customer-specific)
|
|
275
|
+
- seller_contract_id: Seller contract ID (seller-specific)
|
|
276
|
+
- zoho_desk_ticket_id: Zoho Desk ticket ID (seller-specific)
|
|
277
|
+
- is_key_account: Key account flag (seller-specific)
|
|
278
|
+
- is_seller_in_golden_list: Golden list flag (seller-specific)
|
|
279
|
+
- suggested_team_routing: Suggested team routing (P4-P5 only)
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Dictionary of kwargs ready for jira_client.create_issue()
|
|
283
|
+
"""
|
|
284
|
+
business_impact = get_business_impact(impacts_data)
|
|
285
|
+
platform = platforms[0] if platforms else PlatformChoices.ALL.value
|
|
286
|
+
|
|
287
|
+
# Extract optional fields with defaults
|
|
288
|
+
opt = optional_fields or {}
|
|
289
|
+
zendesk_ticket_id = opt.get("zendesk_ticket_id", "")
|
|
290
|
+
seller_contract_id = opt.get("seller_contract_id", "")
|
|
291
|
+
zoho_desk_ticket_id = opt.get("zoho_desk_ticket_id", "")
|
|
292
|
+
is_key_account = opt.get("is_key_account")
|
|
293
|
+
is_seller_in_golden_list = opt.get("is_seller_in_golden_list")
|
|
294
|
+
suggested_team_routing = opt.get("suggested_team_routing")
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
"issuetype": "Incident",
|
|
298
|
+
"summary": title,
|
|
299
|
+
"description": description,
|
|
300
|
+
"priority": priority,
|
|
301
|
+
"reporter": reporter,
|
|
302
|
+
"assignee": None,
|
|
303
|
+
"incident_category": incident_category,
|
|
304
|
+
"environments": environments, # ✅ Always pass environments list
|
|
305
|
+
"platform": platform,
|
|
306
|
+
"business_impact": business_impact,
|
|
307
|
+
"zendesk_ticket_id": zendesk_ticket_id,
|
|
308
|
+
"seller_contract_id": seller_contract_id,
|
|
309
|
+
"zoho_desk_ticket_id": zoho_desk_ticket_id,
|
|
310
|
+
"is_key_account": is_key_account if is_key_account is not None else False,
|
|
311
|
+
"is_seller_in_golden_list": is_seller_in_golden_list if is_seller_in_golden_list is not None else False,
|
|
312
|
+
"suggested_team_routing": suggested_team_routing,
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
|
|
479
316
|
def get_partner_alert_conversations(user_domain: str) -> QuerySet[Conversation]:
|
|
480
317
|
# Get the right channel from tags
|
|
481
318
|
return Conversation.objects.filter(tag__contains=f"raid_alert__{user_domain}")
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from typing import TYPE_CHECKING, Any
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
5
|
|
|
6
6
|
from django.conf import settings
|
|
7
7
|
from django.dispatch.dispatcher import receiver
|
|
8
8
|
|
|
9
9
|
from firefighter.jira_app.client import JiraAPIError, JiraUserNotFoundError
|
|
10
10
|
from firefighter.raid.client import client
|
|
11
|
+
from firefighter.raid.forms import prepare_jira_fields
|
|
11
12
|
from firefighter.raid.models import JiraTicket
|
|
12
13
|
from firefighter.raid.service import get_jira_user_from_user
|
|
13
14
|
from firefighter.slack.messages.slack_messages import (
|
|
@@ -26,31 +27,55 @@ APP_DISPLAY_NAME: str = settings.APP_DISPLAY_NAME
|
|
|
26
27
|
|
|
27
28
|
@receiver(signal=incident_channel_done)
|
|
28
29
|
def create_ticket(
|
|
29
|
-
sender: Any, incident: Incident, channel: IncidentChannel, **kwargs:
|
|
30
|
+
sender: Any, incident: Incident, channel: IncidentChannel, **kwargs: Any
|
|
30
31
|
) -> JiraTicket:
|
|
31
32
|
# pylint: disable=unused-argument
|
|
32
33
|
|
|
34
|
+
# Extract jira_extra_fields and impacts_data from kwargs (passed from unified incident form)
|
|
35
|
+
jira_extra_fields = kwargs.get("jira_extra_fields", {})
|
|
36
|
+
impacts_data = kwargs.get("impacts_data", {})
|
|
37
|
+
logger.info(f"CREATE_TICKET - kwargs keys: {list(kwargs.keys())}")
|
|
38
|
+
logger.info(f"CREATE_TICKET - jira_extra_fields received: {jira_extra_fields}")
|
|
39
|
+
|
|
33
40
|
jira_user = get_jira_user_from_user(incident.created_by)
|
|
34
41
|
account_id = jira_user.id
|
|
35
|
-
|
|
36
|
-
# XXX Custom field with FireFighter ID/link?
|
|
37
|
-
# XXX Set affected environment custom field
|
|
38
|
-
# XXX Set custom field impacted area to group/domain?
|
|
42
|
+
|
|
39
43
|
# Map Impact priority (1-5) to JIRA priority (1-5), fallback to P1 for invalid values
|
|
40
44
|
priority: int = incident.priority.value if 1 <= incident.priority.value <= 5 else 1
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
description=f"""{incident.description}\n
|
|
45
|
+
|
|
46
|
+
# Build enhanced description with incident metadata
|
|
47
|
+
description = f"""{incident.description}\n
|
|
45
48
|
\n
|
|
46
49
|
🧯 This incident has been created for a critical incident. Links below to Slack and {APP_DISPLAY_NAME}.\n
|
|
47
50
|
📦 Incident category: {incident.incident_category.name} ({incident.incident_category.group.name})\n
|
|
48
|
-
{incident.priority.emoji} Priority: {incident.priority.name}\n"""
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
{incident.priority.emoji} Priority: {incident.priority.name}\n"""
|
|
52
|
+
|
|
53
|
+
# Prepare all Jira fields using the common function
|
|
54
|
+
# P1-P3 use first environment only (for backward compatibility)
|
|
55
|
+
environments = jira_extra_fields.get("environments", [incident.environment.value])
|
|
56
|
+
platforms = jira_extra_fields.get("platforms", ["platform-All"])
|
|
57
|
+
|
|
58
|
+
jira_fields = prepare_jira_fields(
|
|
59
|
+
title=incident.title,
|
|
60
|
+
description=description,
|
|
51
61
|
priority=priority,
|
|
62
|
+
reporter=account_id,
|
|
52
63
|
incident_category=incident.incident_category.name,
|
|
64
|
+
environments=[environments[0]] if environments else [incident.environment.value], # P1-P3: first only
|
|
65
|
+
platforms=platforms,
|
|
66
|
+
impacts_data=impacts_data,
|
|
67
|
+
optional_fields={
|
|
68
|
+
"zendesk_ticket_id": jira_extra_fields.get("zendesk_ticket_id", ""),
|
|
69
|
+
"seller_contract_id": jira_extra_fields.get("seller_contract_id", ""),
|
|
70
|
+
"zoho_desk_ticket_id": jira_extra_fields.get("zoho_desk_ticket_id", ""),
|
|
71
|
+
"is_key_account": jira_extra_fields.get("is_key_account"),
|
|
72
|
+
"is_seller_in_golden_list": jira_extra_fields.get("is_seller_in_golden_list"),
|
|
73
|
+
"suggested_team_routing": jira_extra_fields.get("suggested_team_routing"),
|
|
74
|
+
},
|
|
53
75
|
)
|
|
76
|
+
|
|
77
|
+
# Create Jira issue with all prepared fields
|
|
78
|
+
issue = client.create_issue(**jira_fields)
|
|
54
79
|
issue_id = issue.get("id")
|
|
55
80
|
if issue_id is None:
|
|
56
81
|
logger.error(f"Could not create Jira ticket for incident {incident.id}")
|
|
@@ -24,10 +24,11 @@ def incident_updated_close_ticket_when_mitigated_or_postmortem(
|
|
|
24
24
|
updated_fields: list[str],
|
|
25
25
|
**kwargs: Any,
|
|
26
26
|
) -> None:
|
|
27
|
-
# Close Jira ticket if mitigated or
|
|
27
|
+
# Close Jira ticket if mitigated, postmortem, or closed
|
|
28
28
|
if "_status" in updated_fields and incident_update.status in {
|
|
29
|
-
IncidentStatus.
|
|
29
|
+
IncidentStatus.MITIGATED,
|
|
30
30
|
IncidentStatus.POST_MORTEM,
|
|
31
|
+
IncidentStatus.CLOSED,
|
|
31
32
|
}:
|
|
32
33
|
if not hasattr(incident, "jira_ticket") or incident.jira_ticket is None:
|
|
33
34
|
logger.warning(
|
|
@@ -217,6 +217,21 @@ class SlackMessageIncidentDeclaredAnnouncement(SlackMessageSurface):
|
|
|
217
217
|
]
|
|
218
218
|
if hasattr(self.incident, "jira_ticket") and self.incident.jira_ticket:
|
|
219
219
|
fields.append(f":jira_new: <{self.incident.jira_ticket.url}|*Jira ticket*>")
|
|
220
|
+
|
|
221
|
+
# Add custom fields if present
|
|
222
|
+
if hasattr(self.incident, "custom_fields") and self.incident.custom_fields:
|
|
223
|
+
custom_fields = self.incident.custom_fields
|
|
224
|
+
if custom_fields.get("zendesk_ticket_id"):
|
|
225
|
+
fields.append(f":ticket: *Zendesk Ticket:* {custom_fields['zendesk_ticket_id']}")
|
|
226
|
+
if custom_fields.get("seller_contract_id"):
|
|
227
|
+
fields.append(f":memo: *Seller Contract:* {custom_fields['seller_contract_id']}")
|
|
228
|
+
if custom_fields.get("zoho_desk_ticket_id"):
|
|
229
|
+
fields.append(f":ticket: *Zoho Desk Ticket:* {custom_fields['zoho_desk_ticket_id']}")
|
|
230
|
+
if custom_fields.get("is_key_account") is True:
|
|
231
|
+
fields.append(":star: *Key Account*")
|
|
232
|
+
if custom_fields.get("is_seller_in_golden_list") is True:
|
|
233
|
+
fields.append(":medal: *Golden List Seller*")
|
|
234
|
+
|
|
220
235
|
blocks: list[Block] = [
|
|
221
236
|
SectionBlock(
|
|
222
237
|
text=f"{self.incident.priority.emoji} {self.incident.priority.name} - A new incident has been declared:"
|
|
@@ -442,7 +457,7 @@ class SlackMessageIncidentStatusUpdated(SlackMessageSurface):
|
|
|
442
457
|
|
|
443
458
|
if self.in_channel:
|
|
444
459
|
self.title_text = "A new incident update has been posted"
|
|
445
|
-
elif incident.status == IncidentStatus.
|
|
460
|
+
elif incident.status == IncidentStatus.MITIGATED and status_changed:
|
|
446
461
|
self.title_text = f":large_green_circle: Incident #{incident.slack_channel_name} has been {incident.status.label}. :large_green_circle:"
|
|
447
462
|
elif old_priority is not None and old_priority.value > 3:
|
|
448
463
|
self.title_text = f"Incident #{incident.slack_channel_name} has escalated from {old_priority.name} to {incident.priority.name}."
|
|
@@ -509,7 +524,7 @@ class SlackMessageIncidentStatusUpdated(SlackMessageSurface):
|
|
|
509
524
|
value=str(self.incident.id),
|
|
510
525
|
action_id=UpdateStatusModal.open_action,
|
|
511
526
|
)
|
|
512
|
-
if self.in_channel
|
|
527
|
+
if self.in_channel and self.incident.status != IncidentStatus.CLOSED
|
|
513
528
|
else None
|
|
514
529
|
),
|
|
515
530
|
),
|
|
@@ -691,7 +706,7 @@ class SlackMessageDeployWarning(SlackMessageSurface):
|
|
|
691
706
|
blocks = [
|
|
692
707
|
HeaderBlock(
|
|
693
708
|
text=PlainTextObject(
|
|
694
|
-
text=f":warning: Deploy warning {'(Mitigated) ' if self.incident.status == IncidentStatus.
|
|
709
|
+
text=f":warning: Deploy warning {'(Mitigated) ' if self.incident.status == IncidentStatus.MITIGATED else ''}:warning:",
|
|
695
710
|
emoji=True,
|
|
696
711
|
)
|
|
697
712
|
),
|
|
@@ -702,7 +717,7 @@ class SlackMessageDeployWarning(SlackMessageSurface):
|
|
|
702
717
|
),
|
|
703
718
|
]
|
|
704
719
|
|
|
705
|
-
if self.incident.status >= IncidentStatus.
|
|
720
|
+
if self.incident.status >= IncidentStatus.MITIGATED:
|
|
706
721
|
blocks.extend(
|
|
707
722
|
[
|
|
708
723
|
SectionBlock(
|
firefighter/slack/rules.py
CHANGED
|
@@ -31,7 +31,7 @@ def should_publish_in_general_channel(
|
|
|
31
31
|
):
|
|
32
32
|
# If it has just been Mitigated
|
|
33
33
|
if (
|
|
34
|
-
incident.status == IncidentStatus.
|
|
34
|
+
incident.status == IncidentStatus.MITIGATED
|
|
35
35
|
and incident_update is not None
|
|
36
36
|
and incident_update.status is not None
|
|
37
37
|
):
|
|
@@ -45,7 +45,12 @@ def create_incident_slack_conversation(
|
|
|
45
45
|
Args:
|
|
46
46
|
incident (Incident): The incident to open. It should be saved before calling this function, and have its first incident update created.
|
|
47
47
|
|
|
48
|
+
Kwargs:
|
|
49
|
+
jira_extra_fields (dict): Optional dictionary of customer/seller fields for Jira ticket
|
|
50
|
+
|
|
48
51
|
"""
|
|
52
|
+
# Extract jira_extra_fields from kwargs
|
|
53
|
+
jira_extra_fields = _kwargs.get("jira_extra_fields", {})
|
|
49
54
|
channel: IncidentChannel | None = IncidentChannel.objects.create_incident_channel(
|
|
50
55
|
incident=incident
|
|
51
56
|
)
|
|
@@ -131,5 +136,6 @@ def create_incident_slack_conversation(
|
|
|
131
136
|
sender=__name__,
|
|
132
137
|
incident=incident,
|
|
133
138
|
channel=channel,
|
|
139
|
+
jira_extra_fields=jira_extra_fields,
|
|
134
140
|
)
|
|
135
141
|
return None
|
|
@@ -159,6 +159,11 @@ def incident_key_events_updated_handler(
|
|
|
159
159
|
**kwargs: Any,
|
|
160
160
|
) -> None:
|
|
161
161
|
logger.info("Received incident_key_events_updated signal")
|
|
162
|
+
# Skip key events for incidents closed directly
|
|
163
|
+
if incident.closure_reason:
|
|
164
|
+
logger.info(f"Skipping key events update for incident {incident.id} (direct closure)")
|
|
165
|
+
return
|
|
166
|
+
|
|
162
167
|
# Everything in Slack views trigger the Slack handshake, so we delay the import
|
|
163
168
|
from firefighter.slack.views.modals.key_event_message import ( # noqa: PLC0415
|
|
164
169
|
SlackMessageKeyEvents,
|
|
@@ -196,7 +201,8 @@ def publish_status_update(
|
|
|
196
201
|
if (
|
|
197
202
|
incident.ask_for_milestones
|
|
198
203
|
and status_changed
|
|
199
|
-
and incident.status >= IncidentStatus.
|
|
204
|
+
and incident.status >= IncidentStatus.MITIGATED
|
|
205
|
+
and not incident.closure_reason # Don't show key events for direct closures
|
|
200
206
|
):
|
|
201
207
|
from firefighter.slack.views.modals.key_event_message import ( # noqa: PLC0415
|
|
202
208
|
SlackMessageKeyEvents,
|
|
@@ -3,6 +3,10 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from firefighter.slack.views.modals.close import CloseModal, modal_close
|
|
6
|
+
from firefighter.slack.views.modals.closure_reason import (
|
|
7
|
+
ClosureReasonModal,
|
|
8
|
+
modal_closure_reason,
|
|
9
|
+
)
|
|
6
10
|
from firefighter.slack.views.modals.downgrade_workflow import (
|
|
7
11
|
DowngradeWorkflowModal,
|
|
8
12
|
modal_dowgrade_workflow,
|