firefighter-incident 0.0.29__py3-none-any.whl → 0.0.31__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/incidents/forms/update_status.py +76 -28
- firefighter/incidents/views/views.py +6 -2
- firefighter/slack/messages/slack_messages.py +96 -52
- {firefighter_incident-0.0.29.dist-info → firefighter_incident-0.0.31.dist-info}/METADATA +1 -1
- {firefighter_incident-0.0.29.dist-info → firefighter_incident-0.0.31.dist-info}/RECORD +12 -10
- firefighter_tests/test_incidents/test_forms/test_mitigated_reopening.py +193 -0
- firefighter_tests/test_incidents/test_forms/test_workflow_transitions.py +2 -2
- firefighter_tests/test_slack/messages/test_postmortem_reminder.py +136 -0
- {firefighter_incident-0.0.29.dist-info → firefighter_incident-0.0.31.dist-info}/WHEEL +0 -0
- {firefighter_incident-0.0.29.dist-info → firefighter_incident-0.0.31.dist-info}/entry_points.txt +0 -0
- {firefighter_incident-0.0.29.dist-info → firefighter_incident-0.0.31.dist-info}/licenses/LICENSE +0 -0
firefighter/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 0,
|
|
31
|
+
__version__ = version = '0.0.31'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 0, 31)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -43,9 +43,14 @@ class UpdateStatusForm(forms.Form):
|
|
|
43
43
|
),
|
|
44
44
|
)
|
|
45
45
|
|
|
46
|
-
def __init__(
|
|
46
|
+
def __init__(
|
|
47
|
+
self, *args: Any, incident: Incident | None = None, **kwargs: Any
|
|
48
|
+
) -> None:
|
|
47
49
|
super().__init__(*args, **kwargs)
|
|
48
50
|
|
|
51
|
+
# Store incident for later use in clean()
|
|
52
|
+
self.incident = incident
|
|
53
|
+
|
|
49
54
|
# Dynamically adjust status choices based on incident requirements
|
|
50
55
|
if incident:
|
|
51
56
|
self._set_status_choices(incident)
|
|
@@ -62,8 +67,9 @@ class UpdateStatusForm(forms.Form):
|
|
|
62
67
|
and incident.priority.needs_postmortem
|
|
63
68
|
and incident.environment.value == "PRD"
|
|
64
69
|
)
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
allowed_statuses = self._get_allowed_statuses(
|
|
71
|
+
current_status, requires_postmortem=requires_postmortem
|
|
72
|
+
)
|
|
67
73
|
|
|
68
74
|
# If we got a list of enum values, convert to choices and include current status
|
|
69
75
|
if allowed_statuses:
|
|
@@ -72,10 +78,6 @@ class UpdateStatusForm(forms.Form):
|
|
|
72
78
|
# Convert values to strings to match what Slack sends in form submissions
|
|
73
79
|
choices = [(str(s.value), s.label) for s in allowed_statuses]
|
|
74
80
|
status_field.choices = choices # type: ignore[attr-defined]
|
|
75
|
-
logger.debug(
|
|
76
|
-
f"Set status choices for incident #{incident.id}: {choices} "
|
|
77
|
-
f"(current_status={current_status}, requires_postmortem={requires_postmortem})"
|
|
78
|
-
)
|
|
79
81
|
|
|
80
82
|
def _get_allowed_statuses(
|
|
81
83
|
self, current_status: IncidentStatus, *, requires_postmortem: bool
|
|
@@ -84,19 +86,17 @@ class UpdateStatusForm(forms.Form):
|
|
|
84
86
|
|
|
85
87
|
Returns None if choices should be set directly (for default fallback cases).
|
|
86
88
|
"""
|
|
87
|
-
status_field = self.fields["status"]
|
|
88
|
-
|
|
89
89
|
# For incidents requiring post-mortem (P1/P2 in PRD)
|
|
90
90
|
if requires_postmortem:
|
|
91
|
-
return self._get_postmortem_allowed_statuses(current_status
|
|
91
|
+
return self._get_postmortem_allowed_statuses(current_status)
|
|
92
92
|
|
|
93
93
|
# For P3+ incidents (no post-mortem needed)
|
|
94
|
-
return self._get_no_postmortem_allowed_statuses(current_status
|
|
94
|
+
return self._get_no_postmortem_allowed_statuses(current_status)
|
|
95
95
|
|
|
96
96
|
def _get_postmortem_allowed_statuses(
|
|
97
|
-
self, current_status: IncidentStatus
|
|
97
|
+
self, current_status: IncidentStatus
|
|
98
98
|
) -> list[IncidentStatus] | None:
|
|
99
|
-
"""Get allowed statuses for incidents requiring postmortem."""
|
|
99
|
+
"""Get allowed statuses for incidents requiring postmortem (P1/P2)."""
|
|
100
100
|
if current_status == IncidentStatus.OPEN:
|
|
101
101
|
return [IncidentStatus.INVESTIGATING, IncidentStatus.CLOSED]
|
|
102
102
|
if current_status == IncidentStatus.INVESTIGATING:
|
|
@@ -104,18 +104,22 @@ class UpdateStatusForm(forms.Form):
|
|
|
104
104
|
if current_status == IncidentStatus.MITIGATING:
|
|
105
105
|
return [IncidentStatus.MITIGATED]
|
|
106
106
|
if current_status == IncidentStatus.MITIGATED:
|
|
107
|
-
|
|
107
|
+
# P1/P2 can: go to POST_MORTEM (required) OR reopen to INVESTIGATING/MITIGATING (with reason)
|
|
108
|
+
return [
|
|
109
|
+
IncidentStatus.POST_MORTEM, # Required next step
|
|
110
|
+
IncidentStatus.INVESTIGATING, # Reopen option (with reason)
|
|
111
|
+
IncidentStatus.MITIGATING, # Reopen option (with reason)
|
|
112
|
+
]
|
|
108
113
|
if current_status == IncidentStatus.POST_MORTEM:
|
|
109
114
|
return [IncidentStatus.CLOSED]
|
|
110
115
|
|
|
111
|
-
#
|
|
112
|
-
self._set_default_choices(status_field, current_status, IncidentStatus.choices_lte(IncidentStatus.CLOSED))
|
|
116
|
+
# For any other status, return None to use the default choices
|
|
113
117
|
return None
|
|
114
118
|
|
|
115
119
|
def _get_no_postmortem_allowed_statuses(
|
|
116
|
-
self, current_status: IncidentStatus
|
|
120
|
+
self, current_status: IncidentStatus
|
|
117
121
|
) -> list[IncidentStatus] | None:
|
|
118
|
-
"""Get allowed statuses for incidents not requiring postmortem."""
|
|
122
|
+
"""Get allowed statuses for incidents not requiring postmortem (P3/P4/P5)."""
|
|
119
123
|
if current_status == IncidentStatus.OPEN:
|
|
120
124
|
return [IncidentStatus.INVESTIGATING, IncidentStatus.CLOSED]
|
|
121
125
|
if current_status == IncidentStatus.INVESTIGATING:
|
|
@@ -123,12 +127,14 @@ class UpdateStatusForm(forms.Form):
|
|
|
123
127
|
if current_status == IncidentStatus.MITIGATING:
|
|
124
128
|
return [IncidentStatus.MITIGATED]
|
|
125
129
|
if current_status == IncidentStatus.MITIGATED:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
# P3/P4/P5 can: go to CLOSED (normal next step) OR reopen to INVESTIGATING/MITIGATING (with reason)
|
|
131
|
+
return [
|
|
132
|
+
IncidentStatus.CLOSED, # Normal next step for P3+
|
|
133
|
+
IncidentStatus.INVESTIGATING, # Reopen option (with reason)
|
|
134
|
+
IncidentStatus.MITIGATING, # Reopen option (with reason)
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
# For any other status, return None to use the default choices
|
|
132
138
|
return None
|
|
133
139
|
|
|
134
140
|
def _set_default_choices(
|
|
@@ -136,14 +142,21 @@ class UpdateStatusForm(forms.Form):
|
|
|
136
142
|
) -> None:
|
|
137
143
|
"""Set status field choices to default, ensuring current status is included."""
|
|
138
144
|
# Convert default_choices to string keys to match Slack form submissions
|
|
139
|
-
status_field.choices = [
|
|
145
|
+
status_field.choices = [
|
|
146
|
+
(str(choice[0]), choice[1]) for choice in default_choices
|
|
147
|
+
]
|
|
140
148
|
existing_values = {choice[0] for choice in status_field.choices}
|
|
141
149
|
if str(current_status.value) not in existing_values:
|
|
142
150
|
# Insert current status at the beginning
|
|
143
|
-
status_field.choices = [
|
|
151
|
+
status_field.choices = [
|
|
152
|
+
(str(current_status.value), current_status.label),
|
|
153
|
+
*status_field.choices,
|
|
154
|
+
]
|
|
144
155
|
|
|
145
156
|
@staticmethod
|
|
146
|
-
def requires_closure_reason(
|
|
157
|
+
def requires_closure_reason(
|
|
158
|
+
incident: Incident, target_status: IncidentStatus
|
|
159
|
+
) -> bool:
|
|
147
160
|
"""Check if closing this incident to the target status requires a closure reason.
|
|
148
161
|
|
|
149
162
|
Based on the workflow diagram:
|
|
@@ -155,4 +168,39 @@ class UpdateStatusForm(forms.Form):
|
|
|
155
168
|
current_status = incident.status
|
|
156
169
|
|
|
157
170
|
# Require reason if closing from Opened or Investigating (for any priority)
|
|
158
|
-
return current_status.value in {
|
|
171
|
+
return current_status.value in {
|
|
172
|
+
IncidentStatus.OPEN,
|
|
173
|
+
IncidentStatus.INVESTIGATING,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
def clean_message(self) -> str:
|
|
177
|
+
"""Validate message field, ensuring reopening from MITIGATED has sufficient justification."""
|
|
178
|
+
message = self.cleaned_data.get("message", "").strip()
|
|
179
|
+
|
|
180
|
+
# Get the incident and status from form data/initialization
|
|
181
|
+
incident = getattr(self, "incident", None)
|
|
182
|
+
status_value = self.data.get("status") # Use raw form data as cleaned_data may not be ready yet
|
|
183
|
+
|
|
184
|
+
if incident and status_value:
|
|
185
|
+
current_status = incident.status
|
|
186
|
+
|
|
187
|
+
# Convert status value to enum if it's a string/number
|
|
188
|
+
try:
|
|
189
|
+
if isinstance(status_value, str):
|
|
190
|
+
status = IncidentStatus(int(status_value))
|
|
191
|
+
else:
|
|
192
|
+
status = IncidentStatus(status_value)
|
|
193
|
+
except (ValueError, TypeError):
|
|
194
|
+
# Invalid status value, let other validation handle it
|
|
195
|
+
return message
|
|
196
|
+
|
|
197
|
+
# If reopening from MITIGATED to earlier phases, require substantial justification
|
|
198
|
+
if (current_status == IncidentStatus.MITIGATED and
|
|
199
|
+
status in {IncidentStatus.INVESTIGATING, IncidentStatus.MITIGATING} and
|
|
200
|
+
len(message) < 10):
|
|
201
|
+
raise forms.ValidationError(
|
|
202
|
+
"A detailed justification (minimum 10 characters) is required when reopening "
|
|
203
|
+
"an incident from MITIGATED status."
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return message
|
|
@@ -82,7 +82,9 @@ class IncidentStatisticsView(FilterView):
|
|
|
82
82
|
filterset_class = IncidentFilterSet
|
|
83
83
|
model = Incident
|
|
84
84
|
queryset = (
|
|
85
|
-
Incident.objects.select_related(
|
|
85
|
+
Incident.objects.select_related(
|
|
86
|
+
"priority", "incident_category__group", "environment"
|
|
87
|
+
)
|
|
86
88
|
.all()
|
|
87
89
|
.order_by("-id")
|
|
88
90
|
)
|
|
@@ -123,7 +125,9 @@ class DashboardView(generic.ListView[Incident]):
|
|
|
123
125
|
)
|
|
124
126
|
queryset = (
|
|
125
127
|
Incident.objects.filter(_status__lt=IncidentStatus.CLOSED)
|
|
126
|
-
.select_related(
|
|
128
|
+
.select_related(
|
|
129
|
+
"priority", "incident_category__group", "environment", "created_by"
|
|
130
|
+
)
|
|
127
131
|
.order_by("_status", "priority__value")
|
|
128
132
|
.annotate(latest_event_ts=Subquery(sub.values("event_ts")))
|
|
129
133
|
)
|
|
@@ -124,7 +124,10 @@ class SlackMessageIncidentPostMortemReminder(SlackMessageSurface):
|
|
|
124
124
|
),
|
|
125
125
|
)
|
|
126
126
|
)
|
|
127
|
-
elif
|
|
127
|
+
elif (
|
|
128
|
+
hasattr(self.incident, "jira_postmortem_for")
|
|
129
|
+
and self.incident.jira_postmortem_for
|
|
130
|
+
):
|
|
128
131
|
jira_pm = self.incident.jira_postmortem_for
|
|
129
132
|
blocks.append(
|
|
130
133
|
SectionBlock(
|
|
@@ -138,28 +141,30 @@ class SlackMessageIncidentPostMortemReminder(SlackMessageSurface):
|
|
|
138
141
|
)
|
|
139
142
|
|
|
140
143
|
# Continue with remaining steps
|
|
141
|
-
blocks.extend(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
SectionBlock(
|
|
147
|
-
text="4. Once everything has been submitted, close the incident",
|
|
148
|
-
accessory=ButtonElement(
|
|
149
|
-
text="Close incident",
|
|
150
|
-
value=str(self.incident.id),
|
|
151
|
-
action_id=CloseModal.open_action,
|
|
144
|
+
blocks.extend(
|
|
145
|
+
[
|
|
146
|
+
SectionBlock(
|
|
147
|
+
text=f"3. Submit the key events to {APP_DISPLAY_NAME}",
|
|
148
|
+
**accessory_kwargs,
|
|
152
149
|
),
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
150
|
+
SectionBlock(
|
|
151
|
+
text="4. Once everything has been submitted, close the incident",
|
|
152
|
+
accessory=ButtonElement(
|
|
153
|
+
text="Close incident",
|
|
154
|
+
value=str(self.incident.id),
|
|
155
|
+
action_id=CloseModal.open_action,
|
|
156
|
+
),
|
|
157
|
+
),
|
|
158
|
+
DividerBlock(),
|
|
159
|
+
ContextBlock(
|
|
160
|
+
elements=[
|
|
161
|
+
MarkdownTextObject(
|
|
162
|
+
text=":bulb: Updating the status and creating a post-mortem is crucial for the Platform Operations Report presented during the Tech Weekly."
|
|
163
|
+
)
|
|
164
|
+
]
|
|
165
|
+
),
|
|
166
|
+
]
|
|
167
|
+
)
|
|
163
168
|
|
|
164
169
|
if POSTMORTEM_HELP_URL:
|
|
165
170
|
blocks.insert(
|
|
@@ -223,11 +228,7 @@ class SlackMessageIncidentFixedNextActions(SlackMessageSurface):
|
|
|
223
228
|
postmortem_text += " on Jira."
|
|
224
229
|
|
|
225
230
|
blocks.append(
|
|
226
|
-
ContextBlock(
|
|
227
|
-
elements=[
|
|
228
|
-
MarkdownTextObject(text=postmortem_text)
|
|
229
|
-
]
|
|
230
|
-
)
|
|
231
|
+
ContextBlock(elements=[MarkdownTextObject(text=postmortem_text)])
|
|
231
232
|
)
|
|
232
233
|
|
|
233
234
|
return blocks
|
|
@@ -261,11 +262,17 @@ class SlackMessageIncidentDeclaredAnnouncement(SlackMessageSurface):
|
|
|
261
262
|
if hasattr(self.incident, "custom_fields") and self.incident.custom_fields:
|
|
262
263
|
custom_fields = self.incident.custom_fields
|
|
263
264
|
if custom_fields.get("zendesk_ticket_id"):
|
|
264
|
-
fields.append(
|
|
265
|
+
fields.append(
|
|
266
|
+
f":ticket: *Zendesk Ticket:* {custom_fields['zendesk_ticket_id']}"
|
|
267
|
+
)
|
|
265
268
|
if custom_fields.get("seller_contract_id"):
|
|
266
|
-
fields.append(
|
|
269
|
+
fields.append(
|
|
270
|
+
f":memo: *Seller Contract:* {custom_fields['seller_contract_id']}"
|
|
271
|
+
)
|
|
267
272
|
if custom_fields.get("zoho_desk_ticket_id"):
|
|
268
|
-
fields.append(
|
|
273
|
+
fields.append(
|
|
274
|
+
f":ticket: *Zoho Desk Ticket:* {custom_fields['zoho_desk_ticket_id']}"
|
|
275
|
+
)
|
|
269
276
|
if custom_fields.get("is_key_account") is True:
|
|
270
277
|
fields.append(":star: *Key Account*")
|
|
271
278
|
if custom_fields.get("is_seller_in_golden_list") is True:
|
|
@@ -296,10 +303,17 @@ class SlackMessageIncidentDeclaredAnnouncement(SlackMessageSurface):
|
|
|
296
303
|
pm_fields = []
|
|
297
304
|
|
|
298
305
|
if hasattr(self.incident, "postmortem_for") and self.incident.postmortem_for:
|
|
299
|
-
pm_fields.append(
|
|
306
|
+
pm_fields.append(
|
|
307
|
+
f":confluence: <{self.incident.postmortem_for.page_url}|*Confluence Post-mortem*>"
|
|
308
|
+
)
|
|
300
309
|
|
|
301
|
-
if
|
|
302
|
-
|
|
310
|
+
if (
|
|
311
|
+
hasattr(self.incident, "jira_postmortem_for")
|
|
312
|
+
and self.incident.jira_postmortem_for
|
|
313
|
+
):
|
|
314
|
+
pm_fields.append(
|
|
315
|
+
f":jira_new: <{self.incident.jira_postmortem_for.issue_url}|*Jira Post-mortem ({self.incident.jira_postmortem_for.jira_issue_key})*>"
|
|
316
|
+
)
|
|
303
317
|
|
|
304
318
|
if not pm_fields:
|
|
305
319
|
return []
|
|
@@ -582,7 +596,8 @@ class SlackMessageIncidentStatusUpdated(SlackMessageSurface):
|
|
|
582
596
|
value=str(self.incident.id),
|
|
583
597
|
action_id=UpdateStatusModal.open_action,
|
|
584
598
|
)
|
|
585
|
-
if self.in_channel
|
|
599
|
+
if self.in_channel
|
|
600
|
+
and self.incident.status != IncidentStatus.CLOSED
|
|
586
601
|
else None
|
|
587
602
|
),
|
|
588
603
|
),
|
|
@@ -600,6 +615,32 @@ class SlackMessageIncidentStatusUpdated(SlackMessageSurface):
|
|
|
600
615
|
]
|
|
601
616
|
)
|
|
602
617
|
)
|
|
618
|
+
|
|
619
|
+
# Add post mortem reminder if incident is back to MITIGATED and a post mortem already exists
|
|
620
|
+
if (self.incident.status == IncidentStatus.MITIGATED
|
|
621
|
+
and hasattr(self.incident, "jira_postmortem_for")
|
|
622
|
+
and self.incident.jira_postmortem_for):
|
|
623
|
+
jira_pm = self.incident.jira_postmortem_for
|
|
624
|
+
blocks.extend([
|
|
625
|
+
DividerBlock(),
|
|
626
|
+
SectionBlock(
|
|
627
|
+
text=(
|
|
628
|
+
f":memo: *Reminder:* This incident already has an existing post mortem:\n"
|
|
629
|
+
f"• Jira: <{jira_pm.issue_url}|{jira_pm.jira_issue_key}>\n\n"
|
|
630
|
+
f"Please update it with any new findings from this reopening."
|
|
631
|
+
)
|
|
632
|
+
),
|
|
633
|
+
SectionBlock(
|
|
634
|
+
text="Need guidance on how to fill Post-Mortems in Jira?",
|
|
635
|
+
accessory=ButtonElement(
|
|
636
|
+
text="Open documentation",
|
|
637
|
+
url="https://manomano.atlassian.net/wiki/spaces/TC/pages/5639635000/How+to+fill+Post-Mortems+in+Jira",
|
|
638
|
+
value="jira_postmortem_documentation",
|
|
639
|
+
action_id="open_link",
|
|
640
|
+
),
|
|
641
|
+
)
|
|
642
|
+
])
|
|
643
|
+
|
|
603
644
|
return blocks
|
|
604
645
|
|
|
605
646
|
def get_metadata(self) -> Metadata:
|
|
@@ -636,16 +677,12 @@ class SlackMessageIncidentPostMortemCreated(SlackMessageSurface):
|
|
|
636
677
|
|
|
637
678
|
# Add Confluence link if available
|
|
638
679
|
if hasattr(self.incident, "postmortem_for"):
|
|
639
|
-
parts.append(
|
|
640
|
-
f"• Confluence: {self.incident.postmortem_for.page_url}"
|
|
641
|
-
)
|
|
680
|
+
parts.append(f"• Confluence: {self.incident.postmortem_for.page_url}")
|
|
642
681
|
|
|
643
682
|
# Add Jira link if available
|
|
644
683
|
if hasattr(self.incident, "jira_postmortem_for"):
|
|
645
684
|
jira_pm = self.incident.jira_postmortem_for
|
|
646
|
-
parts.append(
|
|
647
|
-
f"• Jira: {jira_pm.issue_url} ({jira_pm.jira_issue_key})"
|
|
648
|
-
)
|
|
685
|
+
parts.append(f"• Jira: {jira_pm.issue_url} ({jira_pm.jira_issue_key})")
|
|
649
686
|
|
|
650
687
|
return "\n".join(parts)
|
|
651
688
|
|
|
@@ -653,7 +690,10 @@ class SlackMessageIncidentPostMortemCreated(SlackMessageSurface):
|
|
|
653
690
|
blocks: list[Block] = [SectionBlock(text=self.get_text())]
|
|
654
691
|
|
|
655
692
|
# Add documentation link if Jira post-mortem exists
|
|
656
|
-
if
|
|
693
|
+
if (
|
|
694
|
+
hasattr(self.incident, "jira_postmortem_for")
|
|
695
|
+
and self.incident.jira_postmortem_for
|
|
696
|
+
):
|
|
657
697
|
blocks.append(
|
|
658
698
|
SectionBlock(
|
|
659
699
|
text="Need guidance on how to fill Post-Mortems in Jira? See our documentation",
|
|
@@ -707,7 +747,9 @@ class SlackMessageIncidentPostMortemCreatedAnnouncement(SlackMessageSurface):
|
|
|
707
747
|
SectionBlock(
|
|
708
748
|
text=f"📔 *Post-mortem created for incident #{self.incident.id}*"
|
|
709
749
|
),
|
|
710
|
-
SectionBlock(
|
|
750
|
+
SectionBlock(
|
|
751
|
+
text=f"*{shorten(self.incident.title, 2995, placeholder='...')}*"
|
|
752
|
+
),
|
|
711
753
|
DividerBlock(),
|
|
712
754
|
SectionBlock(fields=fields),
|
|
713
755
|
]
|
|
@@ -770,17 +812,19 @@ class SlackMessagePostMortemReminder5Days(SlackMessageSurface):
|
|
|
770
812
|
blocks.append(ActionsBlock(elements=pm_links))
|
|
771
813
|
|
|
772
814
|
# Add action buttons
|
|
773
|
-
blocks.extend(
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
815
|
+
blocks.extend(
|
|
816
|
+
[
|
|
817
|
+
DividerBlock(),
|
|
818
|
+
SectionBlock(
|
|
819
|
+
text="Update the incident status or close it once the post-mortem is complete.",
|
|
820
|
+
accessory=ButtonElement(
|
|
821
|
+
text="Update status",
|
|
822
|
+
value=str(self.incident.id),
|
|
823
|
+
action_id=UpdateStatusModal.open_action,
|
|
824
|
+
),
|
|
781
825
|
),
|
|
782
|
-
|
|
783
|
-
|
|
826
|
+
]
|
|
827
|
+
)
|
|
784
828
|
|
|
785
829
|
return blocks
|
|
786
830
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: firefighter-incident
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.31
|
|
4
4
|
Summary: Incident Management tool made for Slack using Django
|
|
5
5
|
Project-URL: Repository, https://github.com/ManoManoTech/firefighter-incident
|
|
6
6
|
Project-URL: Documentation, https://manomanotech.github.io/firefighter-incident/latest/
|
|
@@ -6,7 +6,7 @@ gunicorn.conf.py,sha256=vHsTGjaKOr8FDMp6fTKYTX4AtokmPgYvvt5Mr0Q6APc,273
|
|
|
6
6
|
main.py,sha256=CsbprHoOYhjCLpTJmq9Z_aRYFoFgWxoz2pDLuwm8Eqg,1558
|
|
7
7
|
manage.py,sha256=5ivHGD13C6nJ8QvltKsJ9T9akA5he8da70HLWaEP3k8,689
|
|
8
8
|
firefighter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
firefighter/_version.py,sha256=
|
|
9
|
+
firefighter/_version.py,sha256=nmSHmaFWTuD8SY4lecbyWq4JEPaihzn-zXy2K9lJGYo,706
|
|
10
10
|
firefighter/api/__init__.py,sha256=JQW0Bv6xwGqy7ioxx3h6UGMzkkJ4DntDpbvV1Ncgi8k,136
|
|
11
11
|
firefighter/api/admin.py,sha256=x9Ysy-GiYjb0rynmFdS9g56e6n24fkN0ouGy5QD9Yrc,4629
|
|
12
12
|
firefighter/api/apps.py,sha256=P5uU1_gMrDfzurdMbfqw1Bnb2uNKKcMq17WBPg2sLhc,204
|
|
@@ -145,7 +145,7 @@ firefighter/incidents/forms/select_impact.py,sha256=pH7neqP3d4Bxol4FuizD0Zcp6OP5
|
|
|
145
145
|
firefighter/incidents/forms/unified_incident.py,sha256=3xDB3IFJVwrRe9C_G52SjQ9-Xeqe1ivAGb0e8xtXJaY,19564
|
|
146
146
|
firefighter/incidents/forms/update_key_events.py,sha256=1Xmnxe5OgZqLFS2HmMzQm3VGFPQipsdrLgKSwdh-fKc,4441
|
|
147
147
|
firefighter/incidents/forms/update_roles.py,sha256=Q26UPfwAj-8N23RNZLQkvmHGnS1_j_X5KQWjJmPjMKY,3635
|
|
148
|
-
firefighter/incidents/forms/update_status.py,sha256=
|
|
148
|
+
firefighter/incidents/forms/update_status.py,sha256=j9kGPaDECTrsZogBlgP5MbZdpDIkq3x6aS4cllq0quU,8529
|
|
149
149
|
firefighter/incidents/forms/utils.py,sha256=15e_dBebVd9SvX03DYd0FyZ8s0YpxyBlZfIzEZattwg,4267
|
|
150
150
|
firefighter/incidents/management/__init__.py,sha256=A2LtnedT5NvTcNAN5nXMkPwK56JBNLuptcyObvq7zcc,40
|
|
151
151
|
firefighter/incidents/management/commands/__init__.py,sha256=wc5DFEklUo-wB-6VAAmsV5UTbo5s3t936Lu61z4lojs,29
|
|
@@ -264,7 +264,7 @@ firefighter/incidents/views/date_filter.py,sha256=fUhTjkBulMokI5tAHuqNDVv1dyspjm
|
|
|
264
264
|
firefighter/incidents/views/date_utils.py,sha256=tiRTlh7PmRv4eAH0asiSX3Gn7ajsal9egm4S1d7s3_s,5759
|
|
265
265
|
firefighter/incidents/views/errors.py,sha256=yDuH0YOdGf-voVNEC51yR9Ie3OU-az7g2EqWs_uV1Kk,7855
|
|
266
266
|
firefighter/incidents/views/reports.py,sha256=1Iegx04w-oHw4cj7u9w2_s7T_e9FH5I6RRPTwDZwZhg,20973
|
|
267
|
-
firefighter/incidents/views/views.py,sha256=
|
|
267
|
+
firefighter/incidents/views/views.py,sha256=4AR3i9SiC9AnrzZCRIhx147rus4_dXS0IRWsQ8ilkjg,11031
|
|
268
268
|
firefighter/incidents/views/components/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
269
269
|
firefighter/incidents/views/components/details.py,sha256=gFEezmL1TcVYnM_ryLNNMaynuIdjYV31Qzx_GfzrQiA,1040
|
|
270
270
|
firefighter/incidents/views/components/list.py,sha256=u8HfXetmdL59h_4AZIhiHmKcmrPRZXgekPfnucB4Rek,2207
|
|
@@ -364,7 +364,7 @@ firefighter/slack/management/commands/generate_manifest.py,sha256=zFWHAC7ioozcDd
|
|
|
364
364
|
firefighter/slack/management/commands/switch_test_users.py,sha256=2KTSvCBxsEvZa61J8p0r3huPNhwuytcj2J7IawwZWpQ,11064
|
|
365
365
|
firefighter/slack/messages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
366
366
|
firefighter/slack/messages/base.py,sha256=biH-YEAaldJ-OLHEs5ZjW-gtUYUbjOqxrAEflqV2XS0,4593
|
|
367
|
-
firefighter/slack/messages/slack_messages.py,sha256=
|
|
367
|
+
firefighter/slack/messages/slack_messages.py,sha256=y3z54F2RH5--YOab0tv7fp18bICe3wHulRq6JZ5NYmE,45341
|
|
368
368
|
firefighter/slack/migrations/0001_initial_oss.py,sha256=XmTPgq7zCME2xDwzRFoVi4OegSIG9eSKoyTNoW05Qtg,12933
|
|
369
369
|
firefighter/slack/migrations/0002_usergroup_tag.py,sha256=098tmGA81mT-R2uhb6uQfZ7gKiRG9bFhEwQ8rrp4SKM,583
|
|
370
370
|
firefighter/slack/migrations/0003_alter_usergroup_tag.py,sha256=ncH3KUWEPZHlbdcAtOJ0KGt5H6EX-cKspTGU3osrAhE,591
|
|
@@ -471,12 +471,13 @@ firefighter_tests/test_incidents/test_forms/conftest.py,sha256=YYF5Lm-Jmt-HM9zt_
|
|
|
471
471
|
firefighter_tests/test_incidents/test_forms/test_closure_reason.py,sha256=H6RObqazFAit_pvo7N-lotiSsLOYMafZIk23A5Wiodg,3533
|
|
472
472
|
firefighter_tests/test_incidents/test_forms/test_form_select_impact.py,sha256=DTaPGrJi8mXHfh7mhvDTKYVvDCxqarILauE59UDlwqo,3210
|
|
473
473
|
firefighter_tests/test_incidents/test_forms/test_form_utils.py,sha256=tisEDCacCrG5usEyPkFLCZeG9ebp9b9N27dcUtGmYA4,2467
|
|
474
|
+
firefighter_tests/test_incidents/test_forms/test_mitigated_reopening.py,sha256=TkcpY6qVlNh8L-gemSpmOmACYb61qf_Iy3XbbRruYsg,8035
|
|
474
475
|
firefighter_tests/test_incidents/test_forms/test_unified_incident_form.py,sha256=tb2LSn4rtGbYblAdYXthD0UlKL4h-_u-Bj8nqvDVlGM,22701
|
|
475
476
|
firefighter_tests/test_incidents/test_forms/test_unified_incident_form_integration.py,sha256=q7NKYeCgFMpfSj3V3zGWFA4wr3OzC4yEGCU1zB5RRsY,33082
|
|
476
477
|
firefighter_tests/test_incidents/test_forms/test_unified_incident_form_p4_p5.py,sha256=3MUWwvoZiXZFhoDmYzBS-1lPS8tj28ZF_f30dzsFOZY,15937
|
|
477
478
|
firefighter_tests/test_incidents/test_forms/test_update_key_events.py,sha256=rHRGRU9iFXDdMr_kK3pMB7gyeZuMf7Dyq8bRZkddBC4,1644
|
|
478
479
|
firefighter_tests/test_incidents/test_forms/test_update_status_workflow.py,sha256=q0xXU2BbBG8B0uvvyBWlo4HM8ckbcNAP05Fq8oJNtOw,16270
|
|
479
|
-
firefighter_tests/test_incidents/test_forms/test_workflow_transitions.py,sha256=
|
|
480
|
+
firefighter_tests/test_incidents/test_forms/test_workflow_transitions.py,sha256=4-2IUDRePTsr9cXeN2-sq0wBxN2MjsgJrS3SI-NeR4Y,7298
|
|
480
481
|
firefighter_tests/test_incidents/test_models/test_incident_category.py,sha256=aRoBOhb8fNjLF9CMPZ1FXM8AT51Cd80XPsY2Y3wHY_M,5701
|
|
481
482
|
firefighter_tests/test_incidents/test_models/test_incident_model.py,sha256=AWyWfQYcHNP9GPizIo0wRxNGTJTEJnAwNSd4UmRq-dk,8626
|
|
482
483
|
firefighter_tests/test_incidents/test_models/test_migrations/test_incident_migrations.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -509,6 +510,7 @@ firefighter_tests/test_slack/test_conversation_tags.py,sha256=nNqTZRRBfF6Z4wpFSY
|
|
|
509
510
|
firefighter_tests/test_slack/test_signals_downgrade.py,sha256=mgl4H5vwr2kImf6g4IZbhv7YEPmMzbYSaVr8E6taL88,5420
|
|
510
511
|
firefighter_tests/test_slack/test_slack_utils.py,sha256=9PLobMNXh3xDyFuwzcQFpKJhe4j__sIgf_WRHIpANJw,3957
|
|
511
512
|
firefighter_tests/test_slack/messages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
513
|
+
firefighter_tests/test_slack/messages/test_postmortem_reminder.py,sha256=ZMFMankzpgwZFDP-OWAE5c8sHeT_sHhX_2YH-3Y1lcQ,5053
|
|
512
514
|
firefighter_tests/test_slack/messages/test_slack_messages.py,sha256=uyxfeAy1BQxx1zcCzlSJWn5YF1EnH-5Kt2XoIn9dekM,17484
|
|
513
515
|
firefighter_tests/test_slack/test_models/test_conversations.py,sha256=t3ttmgwiu7c-N55iU3XZPmrkEhvkTzJoXszJncy4Bts,793
|
|
514
516
|
firefighter_tests/test_slack/test_models/test_incident_channel.py,sha256=qWoGe9iadmK6-R8usWvjH87AHRkvhG_dHQeC3kHeJrs,17487
|
|
@@ -526,8 +528,8 @@ firefighter_tests/test_slack/views/modals/test_send_sos.py,sha256=_rE6jD-gOzcGyh
|
|
|
526
528
|
firefighter_tests/test_slack/views/modals/test_status.py,sha256=oQzPfwdg2tkbo9nfkO1GfS3WydxqSC6vy1AZjZDKT30,2226
|
|
527
529
|
firefighter_tests/test_slack/views/modals/test_update_status.py,sha256=vbHGx6dkM_0swE1vJ0HrkhI1oJzD_WHZuIQ-_arAxXo,55686
|
|
528
530
|
firefighter_tests/test_slack/views/modals/test_utils.py,sha256=DJd2n9q6fFu8UuCRdiq9U_Cn19MdnC5c-ydLLrk6rkc,5218
|
|
529
|
-
firefighter_incident-0.0.
|
|
530
|
-
firefighter_incident-0.0.
|
|
531
|
-
firefighter_incident-0.0.
|
|
532
|
-
firefighter_incident-0.0.
|
|
533
|
-
firefighter_incident-0.0.
|
|
531
|
+
firefighter_incident-0.0.31.dist-info/METADATA,sha256=PXaBxOpByn9P2xmf3hHCF7wgTHOVU8GolikIDgNf5QM,5570
|
|
532
|
+
firefighter_incident-0.0.31.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
533
|
+
firefighter_incident-0.0.31.dist-info/entry_points.txt,sha256=c13meJbv7YNmYz7MipMOQwzQ5IeFOPXUBYAJ44XMQsM,61
|
|
534
|
+
firefighter_incident-0.0.31.dist-info/licenses/LICENSE,sha256=krRiGp-a9-1nH1bWpBEdxyTKLhjLmn6DMVVoIb0zF90,1087
|
|
535
|
+
firefighter_incident-0.0.31.dist-info/RECORD,,
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from django.test import TestCase
|
|
7
|
+
|
|
8
|
+
from firefighter.incidents.enums import IncidentStatus
|
|
9
|
+
from firefighter.incidents.factories import IncidentFactory
|
|
10
|
+
from firefighter.incidents.forms.update_status import UpdateStatusForm
|
|
11
|
+
from firefighter.incidents.models import Environment, Priority
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_unique_priority(value: int, *, needs_postmortem: bool = False) -> Priority:
|
|
15
|
+
"""Create a Priority with unique constraints handled properly."""
|
|
16
|
+
return Priority.objects.create(
|
|
17
|
+
value=value,
|
|
18
|
+
name=f"Priority-{value}-{uuid.uuid4().hex[:8]}",
|
|
19
|
+
order=value,
|
|
20
|
+
needs_postmortem=needs_postmortem,
|
|
21
|
+
description=f"Test priority {value}"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def create_unique_environment(value: str, *, exact_value: bool = False) -> Environment:
|
|
26
|
+
"""Create an Environment with unique constraints handled properly.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
value: Base value for the environment
|
|
30
|
+
exact_value: If True, use the value exactly and get_or_create (for testing specific logic)
|
|
31
|
+
"""
|
|
32
|
+
if exact_value:
|
|
33
|
+
# For testing specific environment logic, use exact value with get_or_create
|
|
34
|
+
# This handles the case where fixtures already created an environment with this value
|
|
35
|
+
unique_suffix = uuid.uuid4().hex[:8]
|
|
36
|
+
environment, _ = Environment.objects.get_or_create(
|
|
37
|
+
value=value, # Exact value needed for logic testing
|
|
38
|
+
defaults={
|
|
39
|
+
"name": f"Environment {value} {unique_suffix}",
|
|
40
|
+
"description": f"Test environment {value} {unique_suffix}"
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
return environment
|
|
44
|
+
|
|
45
|
+
# Default case: create unique environment
|
|
46
|
+
unique_suffix = uuid.uuid4().hex[:8]
|
|
47
|
+
return Environment.objects.create(
|
|
48
|
+
value=f"{value}-{unique_suffix}",
|
|
49
|
+
name=f"Environment {value} {unique_suffix}",
|
|
50
|
+
description=f"Test environment {value} {unique_suffix}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.mark.django_db
|
|
55
|
+
class TestMitigatedReopeningWorkflow(TestCase):
|
|
56
|
+
"""Test the new workflow allowing reopening from MITIGATED status."""
|
|
57
|
+
|
|
58
|
+
def test_p1_mitigated_allows_reopening_and_postmortem(self):
|
|
59
|
+
"""P1 incidents should be able to go to POST_MORTEM OR reopen to INVESTIGATING/MITIGATING."""
|
|
60
|
+
# Create P1 priority with needs_postmortem=True
|
|
61
|
+
priority = create_unique_priority(value=1001, needs_postmortem=True)
|
|
62
|
+
# IMPORTANT: Use exact "PRD" value for environment since requires_postmortem logic checks for exact match
|
|
63
|
+
environment = create_unique_environment("PRD", exact_value=True)
|
|
64
|
+
|
|
65
|
+
incident = IncidentFactory.create(
|
|
66
|
+
_status=IncidentStatus.MITIGATED,
|
|
67
|
+
priority=priority,
|
|
68
|
+
environment=environment
|
|
69
|
+
)
|
|
70
|
+
form = UpdateStatusForm(incident=incident)
|
|
71
|
+
|
|
72
|
+
# Get available statuses
|
|
73
|
+
status_choices = dict(form.fields["status"].choices)
|
|
74
|
+
available_statuses = {IncidentStatus(int(k)) for k in status_choices}
|
|
75
|
+
|
|
76
|
+
# P1 incidents should have POST_MORTEM (required for P1/P2)
|
|
77
|
+
assert IncidentStatus.POST_MORTEM in available_statuses
|
|
78
|
+
# P1 incidents should ALSO have direct reopening options (NEW behavior)
|
|
79
|
+
assert IncidentStatus.INVESTIGATING in available_statuses
|
|
80
|
+
assert IncidentStatus.MITIGATING in available_statuses
|
|
81
|
+
|
|
82
|
+
def test_can_reopen_from_mitigated_to_mitigating_p3(self):
|
|
83
|
+
"""P3 incidents can return from MITIGATED to MITIGATING."""
|
|
84
|
+
priority = create_unique_priority(value=3001, needs_postmortem=False)
|
|
85
|
+
environment = create_unique_environment("STG")
|
|
86
|
+
|
|
87
|
+
incident = IncidentFactory.create(
|
|
88
|
+
_status=IncidentStatus.MITIGATED,
|
|
89
|
+
priority=priority,
|
|
90
|
+
environment=environment
|
|
91
|
+
)
|
|
92
|
+
form = UpdateStatusForm(incident=incident)
|
|
93
|
+
|
|
94
|
+
# Get available statuses
|
|
95
|
+
status_choices = dict(form.fields["status"].choices)
|
|
96
|
+
available_statuses = {IncidentStatus(int(k)) for k in status_choices}
|
|
97
|
+
|
|
98
|
+
# Should include both return options and CLOSED
|
|
99
|
+
assert IncidentStatus.INVESTIGATING in available_statuses
|
|
100
|
+
assert IncidentStatus.MITIGATING in available_statuses
|
|
101
|
+
assert IncidentStatus.CLOSED in available_statuses
|
|
102
|
+
# P3 should not include POST_MORTEM
|
|
103
|
+
assert IncidentStatus.POST_MORTEM not in available_statuses
|
|
104
|
+
|
|
105
|
+
def test_form_validation_requires_message_for_reopening(self):
|
|
106
|
+
"""Form validation requires message when reopening from MITIGATED."""
|
|
107
|
+
priority = create_unique_priority(value=5001, needs_postmortem=False)
|
|
108
|
+
environment = create_unique_environment("INT")
|
|
109
|
+
|
|
110
|
+
incident = IncidentFactory.create(
|
|
111
|
+
_status=IncidentStatus.MITIGATED,
|
|
112
|
+
priority=priority,
|
|
113
|
+
environment=environment
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Test without message - should fail
|
|
117
|
+
form_data = {
|
|
118
|
+
"status": str(IncidentStatus.INVESTIGATING.value),
|
|
119
|
+
"priority": str(incident.priority.id),
|
|
120
|
+
"incident_category": str(incident.incident_category.id),
|
|
121
|
+
"message": "", # Empty message should cause validation error
|
|
122
|
+
}
|
|
123
|
+
form = UpdateStatusForm(data=form_data, incident=incident)
|
|
124
|
+
|
|
125
|
+
assert form.is_valid() is False, f"Form should be invalid but got valid=True, errors={form.errors}"
|
|
126
|
+
assert "message" in form.errors
|
|
127
|
+
|
|
128
|
+
def test_form_validation_requires_minimum_message_length(self):
|
|
129
|
+
"""Form validation requires minimum message length for reopening."""
|
|
130
|
+
priority = create_unique_priority(value=5002, needs_postmortem=False)
|
|
131
|
+
environment = create_unique_environment("INT")
|
|
132
|
+
|
|
133
|
+
incident = IncidentFactory.create(
|
|
134
|
+
_status=IncidentStatus.MITIGATED,
|
|
135
|
+
priority=priority,
|
|
136
|
+
environment=environment
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Test with message too short - should fail
|
|
140
|
+
form_data = {
|
|
141
|
+
"status": str(IncidentStatus.INVESTIGATING.value),
|
|
142
|
+
"priority": str(incident.priority.id),
|
|
143
|
+
"incident_category": str(incident.incident_category.id),
|
|
144
|
+
"message": "short", # Less than 10 chars
|
|
145
|
+
}
|
|
146
|
+
form = UpdateStatusForm(data=form_data, incident=incident)
|
|
147
|
+
|
|
148
|
+
assert form.is_valid() is False
|
|
149
|
+
assert "message" in form.errors
|
|
150
|
+
|
|
151
|
+
def test_form_validation_accepts_valid_reopening_message(self):
|
|
152
|
+
"""Form validation accepts valid message for reopening."""
|
|
153
|
+
priority = create_unique_priority(value=5003, needs_postmortem=False)
|
|
154
|
+
environment = create_unique_environment("INT")
|
|
155
|
+
|
|
156
|
+
incident = IncidentFactory.create(
|
|
157
|
+
_status=IncidentStatus.MITIGATED,
|
|
158
|
+
priority=priority,
|
|
159
|
+
environment=environment
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Test with valid message - should pass
|
|
163
|
+
form_data = {
|
|
164
|
+
"status": str(IncidentStatus.INVESTIGATING.value),
|
|
165
|
+
"priority": str(incident.priority.id),
|
|
166
|
+
"incident_category": str(incident.incident_category.id),
|
|
167
|
+
"message": "Investigation revealed additional issues requiring further analysis",
|
|
168
|
+
}
|
|
169
|
+
form = UpdateStatusForm(data=form_data, incident=incident)
|
|
170
|
+
|
|
171
|
+
assert form.is_valid() is True
|
|
172
|
+
|
|
173
|
+
def test_form_validation_normal_transitions_unaffected(self):
|
|
174
|
+
"""Normal transitions not from MITIGATED should be unaffected."""
|
|
175
|
+
priority = create_unique_priority(value=5004, needs_postmortem=False)
|
|
176
|
+
environment = create_unique_environment("INT")
|
|
177
|
+
|
|
178
|
+
# Test from INVESTIGATING to MITIGATING (normal flow)
|
|
179
|
+
incident = IncidentFactory.create(
|
|
180
|
+
_status=IncidentStatus.INVESTIGATING,
|
|
181
|
+
priority=priority,
|
|
182
|
+
environment=environment
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
form_data = {
|
|
186
|
+
"status": str(IncidentStatus.MITIGATING.value),
|
|
187
|
+
"priority": str(incident.priority.id),
|
|
188
|
+
"incident_category": str(incident.incident_category.id),
|
|
189
|
+
"message": "", # Empty message should be OK for normal transitions
|
|
190
|
+
}
|
|
191
|
+
form = UpdateStatusForm(data=form_data, incident=incident)
|
|
192
|
+
|
|
193
|
+
assert form.is_valid() is True
|
|
@@ -50,7 +50,7 @@ class TestCompleteWorkflowTransitions(TestCase):
|
|
|
50
50
|
(IncidentStatus.OPEN, [IncidentStatus.INVESTIGATING, IncidentStatus.CLOSED]),
|
|
51
51
|
(IncidentStatus.INVESTIGATING, [IncidentStatus.MITIGATING, IncidentStatus.CLOSED]),
|
|
52
52
|
(IncidentStatus.MITIGATING, [IncidentStatus.MITIGATED]),
|
|
53
|
-
(IncidentStatus.MITIGATED, [IncidentStatus.POST_MORTEM]),
|
|
53
|
+
(IncidentStatus.MITIGATED, [IncidentStatus.POST_MORTEM, IncidentStatus.INVESTIGATING, IncidentStatus.MITIGATING]),
|
|
54
54
|
(IncidentStatus.POST_MORTEM, [IncidentStatus.CLOSED]),
|
|
55
55
|
]
|
|
56
56
|
|
|
@@ -99,7 +99,7 @@ class TestCompleteWorkflowTransitions(TestCase):
|
|
|
99
99
|
(IncidentStatus.OPEN, [IncidentStatus.INVESTIGATING, IncidentStatus.CLOSED]),
|
|
100
100
|
(IncidentStatus.INVESTIGATING, [IncidentStatus.MITIGATING, IncidentStatus.CLOSED]),
|
|
101
101
|
(IncidentStatus.MITIGATING, [IncidentStatus.MITIGATED]),
|
|
102
|
-
(IncidentStatus.MITIGATED, [IncidentStatus.CLOSED]),
|
|
102
|
+
(IncidentStatus.MITIGATED, [IncidentStatus.CLOSED, IncidentStatus.INVESTIGATING, IncidentStatus.MITIGATING]),
|
|
103
103
|
]
|
|
104
104
|
|
|
105
105
|
for current_status, expected_statuses in transitions:
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from firefighter.incidents.enums import IncidentStatus
|
|
6
|
+
from firefighter.incidents.factories import IncidentFactory, UserFactory
|
|
7
|
+
from firefighter.incidents.models import IncidentUpdate
|
|
8
|
+
from firefighter.jira_app.models import JiraPostMortem
|
|
9
|
+
from firefighter.slack.factories import IncidentChannelFactory
|
|
10
|
+
from firefighter.slack.messages.slack_messages import SlackMessageIncidentStatusUpdated
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.mark.django_db
|
|
14
|
+
class TestSlackMessagePostMortemReminder:
|
|
15
|
+
"""Test post mortem reminder functionality in status update messages."""
|
|
16
|
+
|
|
17
|
+
def test_shows_postmortem_reminder_when_returning_to_mitigated_with_existing_postmortem(self):
|
|
18
|
+
"""Test that post mortem reminder is shown when incident returns to MITIGATED and has existing post mortem."""
|
|
19
|
+
# Create an incident in MITIGATED status
|
|
20
|
+
incident = IncidentFactory.create(_status=IncidentStatus.MITIGATED)
|
|
21
|
+
|
|
22
|
+
# Create an IncidentChannel
|
|
23
|
+
IncidentChannelFactory.create(incident=incident)
|
|
24
|
+
|
|
25
|
+
# Create an existing Jira post mortem
|
|
26
|
+
jira_postmortem = JiraPostMortem.objects.create(
|
|
27
|
+
incident=incident,
|
|
28
|
+
jira_issue_key="INCIDENT-123",
|
|
29
|
+
jira_issue_id="10001",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Create an IncidentUpdate
|
|
33
|
+
user = UserFactory.create()
|
|
34
|
+
incident_update = IncidentUpdate.objects.create(
|
|
35
|
+
incident=incident,
|
|
36
|
+
_status=IncidentStatus.MITIGATED,
|
|
37
|
+
created_by=user,
|
|
38
|
+
message="Incident is back to mitigated after investigation"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Create the message
|
|
42
|
+
message = SlackMessageIncidentStatusUpdated(
|
|
43
|
+
incident=incident,
|
|
44
|
+
incident_update=incident_update,
|
|
45
|
+
in_channel=True,
|
|
46
|
+
status_changed=True,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Get the blocks
|
|
50
|
+
blocks = message.get_blocks()
|
|
51
|
+
|
|
52
|
+
# Convert blocks to text for easier searching
|
|
53
|
+
blocks_text = str(blocks)
|
|
54
|
+
|
|
55
|
+
# Should contain the post mortem reminder
|
|
56
|
+
assert "Reminder" in blocks_text
|
|
57
|
+
assert "already has an existing post mortem" in blocks_text
|
|
58
|
+
assert jira_postmortem.jira_issue_key in blocks_text
|
|
59
|
+
assert jira_postmortem.issue_url in blocks_text
|
|
60
|
+
assert "Open documentation" in blocks_text
|
|
61
|
+
assert "https://manomano.atlassian.net/wiki/spaces/TC/pages/5639635000" in blocks_text
|
|
62
|
+
|
|
63
|
+
def test_no_postmortem_reminder_when_mitigated_without_existing_postmortem(self):
|
|
64
|
+
"""Test that no post mortem reminder is shown when incident is MITIGATED but has no existing post mortem."""
|
|
65
|
+
# Create an incident in MITIGATED status (no post mortem)
|
|
66
|
+
incident = IncidentFactory.create(_status=IncidentStatus.MITIGATED)
|
|
67
|
+
|
|
68
|
+
# Create an IncidentChannel
|
|
69
|
+
IncidentChannelFactory.create(incident=incident)
|
|
70
|
+
|
|
71
|
+
# Create an IncidentUpdate
|
|
72
|
+
user = UserFactory.create()
|
|
73
|
+
incident_update = IncidentUpdate.objects.create(
|
|
74
|
+
incident=incident,
|
|
75
|
+
_status=IncidentStatus.MITIGATED,
|
|
76
|
+
created_by=user
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Create the message
|
|
80
|
+
message = SlackMessageIncidentStatusUpdated(
|
|
81
|
+
incident=incident,
|
|
82
|
+
incident_update=incident_update,
|
|
83
|
+
in_channel=True,
|
|
84
|
+
status_changed=True,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Get the blocks
|
|
88
|
+
blocks = message.get_blocks()
|
|
89
|
+
|
|
90
|
+
# Convert blocks to text for easier searching
|
|
91
|
+
blocks_text = str(blocks)
|
|
92
|
+
|
|
93
|
+
# Should NOT contain the post mortem reminder
|
|
94
|
+
assert "Reminder" not in blocks_text
|
|
95
|
+
assert "already has an existing post mortem" not in blocks_text
|
|
96
|
+
|
|
97
|
+
def test_no_postmortem_reminder_when_not_mitigated_with_existing_postmortem(self):
|
|
98
|
+
"""Test that no post mortem reminder is shown when incident has post mortem but is not in MITIGATED status."""
|
|
99
|
+
# Create an incident in INVESTIGATING status
|
|
100
|
+
incident = IncidentFactory.create(_status=IncidentStatus.INVESTIGATING)
|
|
101
|
+
|
|
102
|
+
# Create an IncidentChannel
|
|
103
|
+
IncidentChannelFactory.create(incident=incident)
|
|
104
|
+
|
|
105
|
+
# Create an existing Jira post mortem
|
|
106
|
+
JiraPostMortem.objects.create(
|
|
107
|
+
incident=incident,
|
|
108
|
+
jira_issue_key="INCIDENT-123",
|
|
109
|
+
jira_issue_id="10001",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Create an IncidentUpdate
|
|
113
|
+
user = UserFactory.create()
|
|
114
|
+
incident_update = IncidentUpdate.objects.create(
|
|
115
|
+
incident=incident,
|
|
116
|
+
_status=IncidentStatus.INVESTIGATING,
|
|
117
|
+
created_by=user
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Create the message
|
|
121
|
+
message = SlackMessageIncidentStatusUpdated(
|
|
122
|
+
incident=incident,
|
|
123
|
+
incident_update=incident_update,
|
|
124
|
+
in_channel=True,
|
|
125
|
+
status_changed=True,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Get the blocks
|
|
129
|
+
blocks = message.get_blocks()
|
|
130
|
+
|
|
131
|
+
# Convert blocks to text for easier searching
|
|
132
|
+
blocks_text = str(blocks)
|
|
133
|
+
|
|
134
|
+
# Should NOT contain the post mortem reminder
|
|
135
|
+
assert "Reminder" not in blocks_text
|
|
136
|
+
assert "already has an existing post mortem" not in blocks_text
|
|
File without changes
|
{firefighter_incident-0.0.29.dist-info → firefighter_incident-0.0.31.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{firefighter_incident-0.0.29.dist-info → firefighter_incident-0.0.31.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|