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
firefighter/raid/forms.py
CHANGED
|
@@ -3,12 +3,10 @@ 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
11
|
from firefighter.incidents.models.priority import Priority
|
|
14
12
|
from firefighter.jira_app.client import (
|
|
@@ -21,15 +19,7 @@ from firefighter.raid.messages import (
|
|
|
21
19
|
SlackMessageRaidCreatedIssue,
|
|
22
20
|
SlackMessageRaidModifiedIssue,
|
|
23
21
|
)
|
|
24
|
-
from firefighter.raid.models import
|
|
25
|
-
from firefighter.raid.service import (
|
|
26
|
-
create_issue_customer,
|
|
27
|
-
create_issue_documentation_request,
|
|
28
|
-
create_issue_feature_request,
|
|
29
|
-
create_issue_internal,
|
|
30
|
-
create_issue_seller,
|
|
31
|
-
get_jira_user_from_user,
|
|
32
|
-
)
|
|
22
|
+
from firefighter.raid.models import JiraTicket
|
|
33
23
|
from firefighter.raid.utils import get_domain_from_email
|
|
34
24
|
from firefighter.slack.models.conversation import Conversation
|
|
35
25
|
|
|
@@ -60,204 +50,11 @@ def initial_priority() -> Priority:
|
|
|
60
50
|
return Priority.objects.get(default=True)
|
|
61
51
|
|
|
62
52
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
title = forms.CharField(
|
|
69
|
-
label="Title",
|
|
70
|
-
max_length=128,
|
|
71
|
-
min_length=10,
|
|
72
|
-
widget=forms.TextInput(attrs={"placeholder": "What's going on?"}),
|
|
73
|
-
)
|
|
74
|
-
description = forms.CharField(
|
|
75
|
-
label="Summary",
|
|
76
|
-
min_length=10,
|
|
77
|
-
max_length=1200,
|
|
78
|
-
)
|
|
79
|
-
suggested_team_routing = forms.ModelChoiceField(
|
|
80
|
-
queryset=FeatureTeam.objects.only("name"),
|
|
81
|
-
label="Feature Team or Train",
|
|
82
|
-
required=True,
|
|
83
|
-
)
|
|
84
|
-
priority = forms.ModelChoiceField(
|
|
85
|
-
label="Priority",
|
|
86
|
-
queryset=Priority.objects.filter(enabled_create=True),
|
|
87
|
-
initial=initial_priority,
|
|
88
|
-
widget=forms.HiddenInput(),
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
field_order = [
|
|
92
|
-
"area",
|
|
93
|
-
"platform",
|
|
94
|
-
"title",
|
|
95
|
-
"description",
|
|
96
|
-
"seller_contract_id",
|
|
97
|
-
"is_key_account",
|
|
98
|
-
"is_seller_in_golden_list",
|
|
99
|
-
"zoho_desk_ticket_id",
|
|
100
|
-
"zendesk_ticket_id",
|
|
101
|
-
"suggested_team_routing",
|
|
102
|
-
]
|
|
103
|
-
|
|
104
|
-
def trigger_incident_workflow(
|
|
105
|
-
self,
|
|
106
|
-
creator: User,
|
|
107
|
-
impacts_data: dict[str, ImpactLevel],
|
|
108
|
-
*args: Any,
|
|
109
|
-
**kwargs: Any,
|
|
110
|
-
) -> Any:
|
|
111
|
-
raise NotImplementedError
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
class CreateNormalCustomerIncidentForm(CreateNormalIncidentFormBase):
|
|
115
|
-
area = forms.ModelChoiceField(queryset=RaidArea.objects.filter(area="Customers"))
|
|
116
|
-
zendesk_ticket_id = forms.CharField(
|
|
117
|
-
label="Zendesk Ticket ID", max_length=128, min_length=2, required=False
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
# XXX business impact: infer from impact/add in impact modal?
|
|
121
|
-
def trigger_incident_workflow(
|
|
122
|
-
self,
|
|
123
|
-
creator: User,
|
|
124
|
-
impacts_data: dict[str, ImpactLevel],
|
|
125
|
-
*args: Never,
|
|
126
|
-
**kwargs: Never,
|
|
127
|
-
) -> None:
|
|
128
|
-
jira_user: JiraUser = get_jira_user_from_user(creator)
|
|
129
|
-
issue_data = create_issue_customer(
|
|
130
|
-
title=self.cleaned_data["title"],
|
|
131
|
-
description=self.cleaned_data["description"],
|
|
132
|
-
priority=self.cleaned_data["priority"].value,
|
|
133
|
-
reporter=jira_user.id,
|
|
134
|
-
platform=self.cleaned_data["platform"],
|
|
135
|
-
business_impact=str(get_business_impact(impacts_data)),
|
|
136
|
-
team_to_be_routed=self.cleaned_data["suggested_team_routing"],
|
|
137
|
-
area=self.cleaned_data["area"].name,
|
|
138
|
-
zendesk_ticket_id=self.cleaned_data["zendesk_ticket_id"],
|
|
139
|
-
labels=[""],
|
|
140
|
-
)
|
|
141
|
-
process_jira_issue(
|
|
142
|
-
issue_data, creator, jira_user=jira_user, impacts_data=impacts_data
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
class CreateRaidDocumentationRequestIncidentForm(CreateNormalIncidentFormBase):
|
|
147
|
-
def trigger_incident_workflow(
|
|
148
|
-
self,
|
|
149
|
-
creator: User,
|
|
150
|
-
impacts_data: dict[str, ImpactLevel],
|
|
151
|
-
*args: Never,
|
|
152
|
-
**kwargs: Never,
|
|
153
|
-
) -> None:
|
|
154
|
-
jira_user: JiraUser = get_jira_user_from_user(creator)
|
|
155
|
-
issue_data = create_issue_documentation_request(
|
|
156
|
-
title=self.cleaned_data["title"],
|
|
157
|
-
description=self.cleaned_data["description"],
|
|
158
|
-
priority=self.cleaned_data["priority"].value,
|
|
159
|
-
reporter=jira_user.id,
|
|
160
|
-
platform=self.cleaned_data["platform"],
|
|
161
|
-
labels=[""],
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
process_jira_issue(
|
|
165
|
-
issue_data, creator, jira_user=jira_user, impacts_data=impacts_data
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
class CreateRaidFeatureRequestIncidentForm(CreateNormalIncidentFormBase):
|
|
170
|
-
def trigger_incident_workflow(
|
|
171
|
-
self,
|
|
172
|
-
creator: User,
|
|
173
|
-
impacts_data: dict[str, ImpactLevel],
|
|
174
|
-
*args: Never,
|
|
175
|
-
**kwargs: Never,
|
|
176
|
-
) -> None:
|
|
177
|
-
jira_user: JiraUser = get_jira_user_from_user(creator)
|
|
178
|
-
issue_data = create_issue_feature_request(
|
|
179
|
-
title=self.cleaned_data["title"],
|
|
180
|
-
description=self.cleaned_data["description"],
|
|
181
|
-
priority=self.cleaned_data["priority"].value,
|
|
182
|
-
reporter=jira_user.id,
|
|
183
|
-
platform=self.cleaned_data["platform"],
|
|
184
|
-
labels=[""],
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
process_jira_issue(
|
|
188
|
-
issue_data, creator, jira_user=jira_user, impacts_data=impacts_data
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
class CreateRaidInternalIncidentForm(CreateNormalIncidentFormBase):
|
|
193
|
-
area = forms.ModelChoiceField(
|
|
194
|
-
queryset=RaidArea.objects.filter(area="Internal").order_by("name")
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
def trigger_incident_workflow(
|
|
198
|
-
self,
|
|
199
|
-
creator: User,
|
|
200
|
-
impacts_data: dict[str, ImpactLevel],
|
|
201
|
-
*args: Never,
|
|
202
|
-
**kwargs: Never,
|
|
203
|
-
) -> None:
|
|
204
|
-
jira_user: JiraUser = get_jira_user_from_user(creator)
|
|
205
|
-
issue_data = create_issue_internal(
|
|
206
|
-
title=self.cleaned_data["title"],
|
|
207
|
-
description=self.cleaned_data["description"],
|
|
208
|
-
priority=self.cleaned_data["priority"].value,
|
|
209
|
-
reporter=jira_user.id,
|
|
210
|
-
platform=self.cleaned_data["platform"],
|
|
211
|
-
business_impact=str(get_business_impact(impacts_data)),
|
|
212
|
-
team_to_be_routed=self.cleaned_data["suggested_team_routing"],
|
|
213
|
-
area=self.cleaned_data["area"].name,
|
|
214
|
-
labels=[""],
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
process_jira_issue(
|
|
218
|
-
issue_data, creator, jira_user=jira_user, impacts_data=impacts_data
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
class RaidCreateIncidentSellerForm(CreateNormalIncidentFormBase):
|
|
223
|
-
area = forms.ModelChoiceField(queryset=RaidArea.objects.filter(area="Sellers"))
|
|
224
|
-
seller_contract_id = forms.CharField(
|
|
225
|
-
label="Seller Contract ID", max_length=128, min_length=0
|
|
226
|
-
)
|
|
227
|
-
is_key_account = forms.BooleanField(label="Is it a Key Account?", required=False)
|
|
228
|
-
is_seller_in_golden_list = forms.BooleanField(
|
|
229
|
-
label="Is the seller in the Golden List?", required=False
|
|
230
|
-
)
|
|
231
|
-
zoho_desk_ticket_id = forms.CharField(
|
|
232
|
-
required=False, label="Zoho Desk Ticket ID", max_length=128, min_length=1
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
def trigger_incident_workflow(
|
|
236
|
-
self,
|
|
237
|
-
creator: User,
|
|
238
|
-
impacts_data: dict[str, ImpactLevel],
|
|
239
|
-
*args: Never,
|
|
240
|
-
**kwargs: Never,
|
|
241
|
-
) -> None:
|
|
242
|
-
jira_user: JiraUser = get_jira_user_from_user(creator)
|
|
243
|
-
issue_data = create_issue_seller(
|
|
244
|
-
title=self.cleaned_data["title"],
|
|
245
|
-
description=self.cleaned_data["description"],
|
|
246
|
-
priority=self.cleaned_data["priority"].value,
|
|
247
|
-
reporter=jira_user.id,
|
|
248
|
-
platform=self.cleaned_data["platform"],
|
|
249
|
-
business_impact=str(get_business_impact(impacts_data)),
|
|
250
|
-
team_to_be_routed=self.cleaned_data["suggested_team_routing"],
|
|
251
|
-
area=self.cleaned_data["area"].name,
|
|
252
|
-
zoho_desk_ticket_id=self.cleaned_data["zoho_desk_ticket_id"],
|
|
253
|
-
is_key_account=self.cleaned_data["is_key_account"],
|
|
254
|
-
is_seller_in_golden_list=self.cleaned_data["is_seller_in_golden_list"],
|
|
255
|
-
seller_contract_id=self.cleaned_data["seller_contract_id"],
|
|
256
|
-
labels=[""],
|
|
257
|
-
)
|
|
258
|
-
process_jira_issue(
|
|
259
|
-
issue_data, creator, jira_user=jira_user, impacts_data=impacts_data
|
|
260
|
-
)
|
|
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.
|
|
261
58
|
|
|
262
59
|
|
|
263
60
|
def process_jira_issue(
|
|
@@ -335,10 +132,15 @@ def alert_slack_new_jira_ticket(
|
|
|
335
132
|
message,
|
|
336
133
|
unfurl_links=False,
|
|
337
134
|
)
|
|
338
|
-
except SlackApiError:
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
135
|
+
except SlackApiError as e:
|
|
136
|
+
if e.response.get("error") == "messages_tab_disabled":
|
|
137
|
+
logger.warning(
|
|
138
|
+
f"User {reporter_user.slack_user} has disabled private messages from bots"
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
logger.exception(
|
|
142
|
+
f"Couldn't send private message to reporter {reporter_user.slack_user}"
|
|
143
|
+
)
|
|
342
144
|
|
|
343
145
|
# Get the right channels from tags
|
|
344
146
|
channels = get_internal_alert_conversations(jira_ticket)
|
|
@@ -442,6 +244,75 @@ def get_business_impact(impacts_data: dict[str, ImpactLevel]) -> str | None:
|
|
|
442
244
|
return impact_form.business_impact_new
|
|
443
245
|
|
|
444
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
|
+
|
|
445
316
|
def get_partner_alert_conversations(user_domain: str) -> QuerySet[Conversation]:
|
|
446
317
|
# Get the right channel from tags
|
|
447
318
|
return Conversation.objects.filter(tag__contains=f"raid_alert__{user_domain}")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Generated by Django 4.2.23 on 2025-09-17 17:49
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
("raid", "0002_featureteam_remove_qualifierrotation_jira_user_and_more"),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.DeleteModel(
|
|
14
|
+
name="RaidArea",
|
|
15
|
+
),
|
|
16
|
+
]
|
firefighter/raid/models.py
CHANGED
|
@@ -58,25 +58,6 @@ class JiraTicketImpact(models.Model):
|
|
|
58
58
|
return f"{self.jira_ticket.key}: {self.impact}"
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
class RaidArea(models.Model):
|
|
62
|
-
id = models.AutoField(primary_key=True)
|
|
63
|
-
name = models.CharField(max_length=128)
|
|
64
|
-
area = models.CharField(
|
|
65
|
-
choices=(
|
|
66
|
-
("Sellers", "Sellers"),
|
|
67
|
-
("Internal", "Internal"),
|
|
68
|
-
("Customers", "Customers"),
|
|
69
|
-
),
|
|
70
|
-
max_length=32,
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
class Meta(TypedModelMeta):
|
|
74
|
-
unique_together = ("name", "area")
|
|
75
|
-
|
|
76
|
-
def __str__(self) -> str:
|
|
77
|
-
return self.name
|
|
78
|
-
|
|
79
|
-
|
|
80
61
|
class FeatureTeam(models.Model):
|
|
81
62
|
id = models.AutoField(primary_key=True)
|
|
82
63
|
name = models.CharField(max_length=80)
|
|
@@ -95,8 +76,8 @@ class FeatureTeam(models.Model):
|
|
|
95
76
|
|
|
96
77
|
@property
|
|
97
78
|
def get_team(self) -> str:
|
|
98
|
-
return "{self.name} {self.jira_project_key}"
|
|
79
|
+
return f"{self.name} {self.jira_project_key}"
|
|
99
80
|
|
|
100
81
|
@property
|
|
101
82
|
def get_key(self) -> str:
|
|
102
|
-
return "{self.jira_project_key}"
|
|
83
|
+
return f"{self.jira_project_key}"
|
firefighter/raid/serializers.py
CHANGED
|
@@ -114,7 +114,7 @@ class LandbotIssueRequestSerializer(serializers.ModelSerializer[JiraTicket]):
|
|
|
114
114
|
),
|
|
115
115
|
help_text="List of labels to be added to the ticket. Labels cannot contain spaces and must not exceed 255 characters.",
|
|
116
116
|
)
|
|
117
|
-
|
|
117
|
+
incident_category = serializers.CharField(
|
|
118
118
|
max_length=128,
|
|
119
119
|
write_only=True,
|
|
120
120
|
allow_null=True,
|
|
@@ -131,7 +131,8 @@ class LandbotIssueRequestSerializer(serializers.ModelSerializer[JiraTicket]):
|
|
|
131
131
|
],
|
|
132
132
|
)
|
|
133
133
|
priority = serializers.IntegerField(
|
|
134
|
-
min_value=1, max_value=
|
|
134
|
+
min_value=1, max_value=5, write_only=True, allow_null=True,
|
|
135
|
+
help_text="Priority level 1-5 (1=Critical, 2=High, 3=Medium, 4=Low, 5=Lowest)"
|
|
135
136
|
)
|
|
136
137
|
business_impact = serializers.ChoiceField(
|
|
137
138
|
write_only=True, choices=["High", "Medium", "Low"], allow_null=True
|
|
@@ -198,7 +199,7 @@ class LandbotIssueRequestSerializer(serializers.ModelSerializer[JiraTicket]):
|
|
|
198
199
|
seller_contract_id=validated_data["seller_contract_id"],
|
|
199
200
|
zoho_desk_ticket_id=validated_data["zoho"],
|
|
200
201
|
platform=validated_data["platform"],
|
|
201
|
-
|
|
202
|
+
incident_category=validated_data["incident_category"],
|
|
202
203
|
business_impact=validated_data["business_impact"],
|
|
203
204
|
environments=validated_data["environments"],
|
|
204
205
|
suggested_team_routing=validated_data["suggested_team_routing"],
|
|
@@ -239,7 +240,7 @@ class LandbotIssueRequestSerializer(serializers.ModelSerializer[JiraTicket]):
|
|
|
239
240
|
"zoho",
|
|
240
241
|
"platform",
|
|
241
242
|
"reporter_email",
|
|
242
|
-
"
|
|
243
|
+
"incident_category",
|
|
243
244
|
"labels",
|
|
244
245
|
"environments",
|
|
245
246
|
"issue_type",
|
firefighter/raid/service.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
from dataclasses import dataclass
|
|
4
5
|
from typing import TYPE_CHECKING
|
|
5
6
|
|
|
6
7
|
from django.conf import settings
|
|
@@ -133,7 +134,7 @@ def create_issue_internal(
|
|
|
133
134
|
platform: str,
|
|
134
135
|
business_impact: str | None,
|
|
135
136
|
team_to_be_routed: str | None,
|
|
136
|
-
|
|
137
|
+
incident_category: str | None,
|
|
137
138
|
) -> JiraObject:
|
|
138
139
|
"""Creates a Jira Incident Issue of type Internal.
|
|
139
140
|
|
|
@@ -146,7 +147,7 @@ def create_issue_internal(
|
|
|
146
147
|
platform (str): Platform of the issue
|
|
147
148
|
business_impact (str): Business impact of the issue
|
|
148
149
|
team_to_be_routed (str): Team to be routed
|
|
149
|
-
|
|
150
|
+
incident_category (str): Incident category of the issue
|
|
150
151
|
"""
|
|
151
152
|
issue = jira_client.create_issue(
|
|
152
153
|
issuetype="Incident",
|
|
@@ -159,23 +160,30 @@ def create_issue_internal(
|
|
|
159
160
|
platform=platform,
|
|
160
161
|
business_impact=business_impact,
|
|
161
162
|
suggested_team_routing=team_to_be_routed,
|
|
162
|
-
|
|
163
|
+
incident_category=incident_category,
|
|
163
164
|
)
|
|
164
165
|
check_issue_id(issue, title=title, reporter=reporter)
|
|
165
166
|
return issue
|
|
166
167
|
|
|
167
168
|
|
|
169
|
+
@dataclass
|
|
170
|
+
class CustomerIssueData:
|
|
171
|
+
"""Data container for customer issue creation parameters."""
|
|
172
|
+
priority: int | None
|
|
173
|
+
labels: list[str] | None
|
|
174
|
+
platform: str
|
|
175
|
+
business_impact: str | None
|
|
176
|
+
team_to_be_routed: str | None
|
|
177
|
+
area: str | None
|
|
178
|
+
zendesk_ticket_id: str | None
|
|
179
|
+
incident_category: str | None = None
|
|
180
|
+
|
|
181
|
+
|
|
168
182
|
def create_issue_customer(
|
|
169
183
|
title: str,
|
|
170
184
|
description: str,
|
|
171
185
|
reporter: str,
|
|
172
|
-
|
|
173
|
-
labels: list[str] | None,
|
|
174
|
-
platform: str,
|
|
175
|
-
business_impact: str | None,
|
|
176
|
-
team_to_be_routed: str | None,
|
|
177
|
-
area: str | None,
|
|
178
|
-
zendesk_ticket_id: str | None,
|
|
186
|
+
issue_data: CustomerIssueData,
|
|
179
187
|
) -> JiraObject:
|
|
180
188
|
"""Creates a Jira Incident issue of type Customer.
|
|
181
189
|
|
|
@@ -183,13 +191,7 @@ def create_issue_customer(
|
|
|
183
191
|
title (str): Summary of the issue
|
|
184
192
|
description (str): Description of the issue
|
|
185
193
|
reporter (str): Jira account id of the reporter
|
|
186
|
-
|
|
187
|
-
labels (list[str]): Labels to add to the issue
|
|
188
|
-
platform (str): Platform of the issue
|
|
189
|
-
business_impact (str): Business impact of the issue
|
|
190
|
-
team_to_be_routed (str): Team to be routed
|
|
191
|
-
area (str): Area of the issue
|
|
192
|
-
zendesk_ticket_id (str): Zendesk ticket id
|
|
194
|
+
issue_data (CustomerIssueData): Container with issue parameters
|
|
193
195
|
"""
|
|
194
196
|
issue = jira_client.create_issue(
|
|
195
197
|
issuetype="Incident",
|
|
@@ -197,13 +199,13 @@ def create_issue_customer(
|
|
|
197
199
|
description=description,
|
|
198
200
|
assignee=None,
|
|
199
201
|
reporter=reporter,
|
|
200
|
-
priority=priority,
|
|
201
|
-
labels=labels,
|
|
202
|
-
platform=platform,
|
|
203
|
-
business_impact=business_impact,
|
|
204
|
-
suggested_team_routing=team_to_be_routed,
|
|
205
|
-
|
|
206
|
-
|
|
202
|
+
priority=issue_data.priority,
|
|
203
|
+
labels=issue_data.labels,
|
|
204
|
+
platform=issue_data.platform,
|
|
205
|
+
business_impact=issue_data.business_impact,
|
|
206
|
+
suggested_team_routing=issue_data.team_to_be_routed,
|
|
207
|
+
zendesk_ticket_id=issue_data.zendesk_ticket_id,
|
|
208
|
+
incident_category=issue_data.incident_category,
|
|
207
209
|
)
|
|
208
210
|
check_issue_id(issue, title=title, reporter=reporter)
|
|
209
211
|
return issue
|
|
@@ -218,7 +220,7 @@ def create_issue_seller( # noqa: PLR0913, PLR0917
|
|
|
218
220
|
platform: str,
|
|
219
221
|
business_impact: str | None,
|
|
220
222
|
team_to_be_routed: str | None,
|
|
221
|
-
|
|
223
|
+
incident_category: str | None,
|
|
222
224
|
seller_contract_id: str | None,
|
|
223
225
|
is_key_account: bool | None, # noqa: FBT001
|
|
224
226
|
is_seller_in_golden_list: bool | None, # noqa: FBT001
|
|
@@ -235,7 +237,7 @@ def create_issue_seller( # noqa: PLR0913, PLR0917
|
|
|
235
237
|
platform (str): Platform of the issue
|
|
236
238
|
business_impact (str): Business impact of the issue
|
|
237
239
|
team_to_be_routed (str): Team to be routed
|
|
238
|
-
|
|
240
|
+
incident_category (str): Incident category of the issue
|
|
239
241
|
seller_contract_id (str): Seller contract id
|
|
240
242
|
is_key_account (bool): Is key account
|
|
241
243
|
is_seller_in_golden_list (bool): Is seller in golden list
|
|
@@ -252,11 +254,11 @@ def create_issue_seller( # noqa: PLR0913, PLR0917
|
|
|
252
254
|
platform=platform,
|
|
253
255
|
business_impact=business_impact,
|
|
254
256
|
suggested_team_routing=team_to_be_routed,
|
|
255
|
-
area=area,
|
|
256
257
|
seller_contract_id=seller_contract_id,
|
|
257
258
|
is_key_account=is_key_account,
|
|
258
259
|
is_seller_in_golden_list=is_seller_in_golden_list,
|
|
259
260
|
zoho_desk_ticket_id=zoho_desk_ticket_id,
|
|
261
|
+
incident_category=incident_category,
|
|
260
262
|
)
|
|
261
263
|
check_issue_id(issue, title=title, reporter=reporter)
|
|
262
264
|
return issue
|
|
@@ -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,29 +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
|
-
#
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
issuetype="Incident",
|
|
42
|
-
summary=incident.title,
|
|
43
|
-
description=f"""{incident.description}\n
|
|
42
|
+
|
|
43
|
+
# Map Impact priority (1-5) to JIRA priority (1-5), fallback to P1 for invalid values
|
|
44
|
+
priority: int = incident.priority.value if 1 <= incident.priority.value <= 5 else 1
|
|
45
|
+
|
|
46
|
+
# Build enhanced description with incident metadata
|
|
47
|
+
description = f"""{incident.description}\n
|
|
44
48
|
\n
|
|
45
49
|
🧯 This incident has been created for a critical incident. Links below to Slack and {APP_DISPLAY_NAME}.\n
|
|
46
|
-
📦
|
|
47
|
-
{incident.priority.emoji} Priority: {incident.priority.name}\n"""
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
📦 Incident category: {incident.incident_category.name} ({incident.incident_category.group.name})\n
|
|
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,
|
|
50
61
|
priority=priority,
|
|
62
|
+
reporter=account_id,
|
|
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
|
+
},
|
|
51
75
|
)
|
|
76
|
+
|
|
77
|
+
# Create Jira issue with all prepared fields
|
|
78
|
+
issue = client.create_issue(**jira_fields)
|
|
52
79
|
issue_id = issue.get("id")
|
|
53
80
|
if issue_id is None:
|
|
54
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(
|
firefighter/raid/utils.py
CHANGED
|
@@ -33,7 +33,7 @@ def get_domain_from_email(email: str) -> str:
|
|
|
33
33
|
if email.count("@") != 1:
|
|
34
34
|
msg = f"Invalid email: {email}"
|
|
35
35
|
raise ValueError(msg)
|
|
36
|
-
domain = email.
|
|
36
|
+
domain = email.rsplit("@", maxsplit=1)[-1]
|
|
37
37
|
if not domain:
|
|
38
38
|
msg = f"Invalid email: {email}"
|
|
39
39
|
raise ValueError(msg)
|
|
@@ -37,7 +37,7 @@ if TYPE_CHECKING:
|
|
|
37
37
|
"zoho": "https://crmplus.zoho.eu/mycrmlink/index.do/cxapp/agent/mycompany/all/tickets/details/123456789",
|
|
38
38
|
"platform": "FR",
|
|
39
39
|
"reporter_email": "john.doe@mycompany.com",
|
|
40
|
-
"
|
|
40
|
+
"incident_category": "Payment Processing",
|
|
41
41
|
"project": "SBI",
|
|
42
42
|
"labels": ["originBot", "ProductsMerge"],
|
|
43
43
|
"environments": ["PRD", "STG"],
|