firefighter-incident 0.0.28__py3-none-any.whl → 0.0.30__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 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.28'
32
- __version_tuple__ = version_tuple = (0, 0, 28)
31
+ __version__ = version = '0.0.30'
32
+ __version_tuple__ = version_tuple = (0, 0, 30)
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__(self, *args: Any, incident: Incident | None = None, **kwargs: Any) -> None:
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
- allowed_statuses = self._get_allowed_statuses(current_status, requires_postmortem=requires_postmortem)
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, status_field)
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, status_field)
94
+ return self._get_no_postmortem_allowed_statuses(current_status)
95
95
 
96
96
  def _get_postmortem_allowed_statuses(
97
- self, current_status: IncidentStatus, status_field: Any
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
- return [IncidentStatus.POST_MORTEM]
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
- # Default: all statuses up to closed
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, status_field: Any
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
- return [IncidentStatus.CLOSED]
127
-
128
- # Default fallback
129
- self._set_default_choices(
130
- status_field, current_status, IncidentStatus.choices_lte_skip_postmortem(IncidentStatus.CLOSED)
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 = [(str(choice[0]), choice[1]) for choice in default_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 = [(str(current_status.value), current_status.label), *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(incident: Incident, target_status: IncidentStatus) -> bool:
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 {IncidentStatus.OPEN, IncidentStatus.INVESTIGATING}
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("priority", "incident_category__group", "environment")
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("priority", "incident_category__group", "environment", "created_by")
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
  )
@@ -148,10 +152,22 @@ class IncidentDetailView(CustomDetailView[Incident]):
148
152
  "conversation",
149
153
  "created_by",
150
154
  ]
151
- if settings.ENABLE_CONFLUENCE:
152
- select_related.append("postmortem_for")
153
- if getattr(settings, "ENABLE_JIRA_POSTMORTEM", False):
154
- select_related.append("jira_postmortem_for")
155
+
156
+ # Always load post-mortem relationships to display existing data
157
+ # even if creation is disabled
158
+ try:
159
+ # Only add if confluence app is installed
160
+ if "firefighter.confluence" in settings.INSTALLED_APPS:
161
+ select_related.append("postmortem_for")
162
+ except ImportError:
163
+ pass
164
+
165
+ try:
166
+ # Only add if jira_app is installed
167
+ if "firefighter.jira_app" in settings.INSTALLED_APPS:
168
+ select_related.append("jira_postmortem_for")
169
+ except ImportError:
170
+ pass
155
171
  queryset = Incident.objects.select_related(*select_related).prefetch_related(
156
172
  Prefetch(
157
173
  "incidentupdate_set",
@@ -124,7 +124,10 @@ class SlackMessageIncidentPostMortemReminder(SlackMessageSurface):
124
124
  ),
125
125
  )
126
126
  )
127
- elif hasattr(self.incident, "jira_postmortem_for") and self.incident.jira_postmortem_for:
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
- SectionBlock(
143
- text=f"3. Submit the key events to {APP_DISPLAY_NAME}",
144
- **accessory_kwargs,
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
- DividerBlock(),
155
- ContextBlock(
156
- elements=[
157
- MarkdownTextObject(
158
- text=":bulb: Updating the status and creating a post-mortem is crucial for the Platform Operations Report presented during the Tech Weekly."
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(f":ticket: *Zendesk Ticket:* {custom_fields['zendesk_ticket_id']}")
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(f":memo: *Seller Contract:* {custom_fields['seller_contract_id']}")
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(f":ticket: *Zoho Desk Ticket:* {custom_fields['zoho_desk_ticket_id']}")
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(f":confluence: <{self.incident.postmortem_for.page_url}|*Confluence Post-mortem*>")
306
+ pm_fields.append(
307
+ f":confluence: <{self.incident.postmortem_for.page_url}|*Confluence Post-mortem*>"
308
+ )
300
309
 
301
- if hasattr(self.incident, "jira_postmortem_for") and self.incident.jira_postmortem_for:
302
- pm_fields.append(f":jira_new: <{self.incident.jira_postmortem_for.issue_url}|*Jira Post-mortem ({self.incident.jira_postmortem_for.jira_issue_key})*>")
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 and self.incident.status != IncidentStatus.CLOSED
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 hasattr(self.incident, "jira_postmortem_for") and self.incident.jira_postmortem_for:
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(text=f"*{shorten(self.incident.title, 2995, placeholder='...')}*"),
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
- DividerBlock(),
775
- SectionBlock(
776
- text="Update the incident status or close it once the post-mortem is complete.",
777
- accessory=ButtonElement(
778
- text="Update status",
779
- value=str(self.incident.id),
780
- action_id=UpdateStatusModal.open_action,
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.28
3
+ Version: 0.0.30
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=rsZxmANBVfE-nN63y-ZZ_71Lkm6YP4u4TgVEBJV3mNM,706
9
+ firefighter/_version.py,sha256=od6_NordEIvGqyI_S2FZ1DYZPTNZflFSs-zacl-pIks,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=7GSno_EqD2Brd6wWcSb3zsP6nz8_mUTXXnl0QCRhv48,6682
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=TnmPeVYXNDU6MSUHqqnbTLYUBpzFsng_fy6qVcxEwVU,10655
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=9-zbxBuzDweKQZiwc2DMdiQcsnd23CGCi4YWhGgJx60,43713
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=priKh7QYZxGDPu2SvPC8pGnqOsZWg5cLkyC40pDvLAU,7184
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.28.dist-info/METADATA,sha256=mM6LIiFAeLibO2jnQEf9KZiEOFlZJT09A3jrCExS5sM,5570
530
- firefighter_incident-0.0.28.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
531
- firefighter_incident-0.0.28.dist-info/entry_points.txt,sha256=c13meJbv7YNmYz7MipMOQwzQ5IeFOPXUBYAJ44XMQsM,61
532
- firefighter_incident-0.0.28.dist-info/licenses/LICENSE,sha256=krRiGp-a9-1nH1bWpBEdxyTKLhjLmn6DMVVoIb0zF90,1087
533
- firefighter_incident-0.0.28.dist-info/RECORD,,
531
+ firefighter_incident-0.0.30.dist-info/METADATA,sha256=a6G3vwNEOB7LZPmCxfrKQmZY4AGVkSXUcxdOFptvE08,5570
532
+ firefighter_incident-0.0.30.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
533
+ firefighter_incident-0.0.30.dist-info/entry_points.txt,sha256=c13meJbv7YNmYz7MipMOQwzQ5IeFOPXUBYAJ44XMQsM,61
534
+ firefighter_incident-0.0.30.dist-info/licenses/LICENSE,sha256=krRiGp-a9-1nH1bWpBEdxyTKLhjLmn6DMVVoIb0zF90,1087
535
+ firefighter_incident-0.0.30.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