firefighter-incident 0.0.26__py3-none-any.whl → 0.0.28__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.
Files changed (32) hide show
  1. firefighter/_version.py +2 -2
  2. firefighter/confluence/models.py +16 -1
  3. firefighter/incidents/management/__init__.py +1 -0
  4. firefighter/incidents/management/commands/__init__.py +1 -0
  5. firefighter/incidents/management/commands/backdate_incident_mitigated.py +94 -0
  6. firefighter/incidents/management/commands/test_postmortem_reminders.py +113 -0
  7. firefighter/incidents/migrations/0030_add_mitigated_at_field.py +22 -0
  8. firefighter/incidents/models/incident.py +43 -8
  9. firefighter/jira_app/service_postmortem.py +13 -0
  10. firefighter/jira_app/signals/postmortem_created.py +108 -46
  11. firefighter/jira_app/templates/jira/postmortem/impact.txt +9 -4
  12. firefighter/slack/messages/slack_messages.py +234 -18
  13. firefighter/slack/migrations/0009_add_postmortem_reminder_periodic_task.py +60 -0
  14. firefighter/slack/rules.py +22 -0
  15. firefighter/slack/tasks/send_postmortem_reminders.py +127 -0
  16. firefighter/slack/views/modals/close.py +113 -3
  17. firefighter/slack/views/modals/closure_reason.py +39 -15
  18. firefighter/slack/views/modals/postmortem.py +75 -7
  19. firefighter/slack/views/modals/update_status.py +4 -4
  20. {firefighter_incident-0.0.26.dist-info → firefighter_incident-0.0.28.dist-info}/METADATA +1 -1
  21. {firefighter_incident-0.0.26.dist-info → firefighter_incident-0.0.28.dist-info}/RECORD +32 -24
  22. firefighter_tests/test_incidents/test_incident_urls.py +4 -0
  23. firefighter_tests/test_incidents/test_models/test_incident_model.py +109 -1
  24. firefighter_tests/test_incidents/test_views/test_incident_detail_view.py +4 -0
  25. firefighter_tests/test_slack/messages/test_slack_messages.py +4 -0
  26. firefighter_tests/test_slack/views/modals/test_close.py +4 -0
  27. firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +109 -26
  28. firefighter_tests/test_slack/views/modals/test_postmortem_modal.py +72 -0
  29. firefighter_tests/test_slack/views/modals/test_update_status.py +45 -51
  30. {firefighter_incident-0.0.26.dist-info → firefighter_incident-0.0.28.dist-info}/WHEEL +0 -0
  31. {firefighter_incident-0.0.26.dist-info → firefighter_incident-0.0.28.dist-info}/entry_points.txt +0 -0
  32. {firefighter_incident-0.0.26.dist-info → firefighter_incident-0.0.28.dist-info}/licenses/LICENSE +0 -0
@@ -115,6 +115,19 @@ class ClosureReasonModal(IncidentSelectableModalMixin, SlackModal):
115
115
  self, ack: Ack, body: dict[str, Any], incident: Incident, user: User
116
116
  ) -> bool | None:
117
117
  """Handle the closure reason modal submission."""
118
+ # Extract form values up-front so validation can account for the submitted reason
119
+ state_values = body["view"]["state"]["values"]
120
+ closure_reason = state_values["closure_reason"]["select_closure_reason"][
121
+ "selected_option"
122
+ ]["value"]
123
+ closure_reference = (
124
+ state_values["closure_reference"]["input_closure_reference"].get(
125
+ "value", ""
126
+ )
127
+ or ""
128
+ )
129
+ message = state_values["closure_message"]["input_closure_message"]["value"]
130
+
118
131
  # For early closure (OPEN/INVESTIGATING), we bypass normal workflow checks
119
132
  # For normal closure (MITIGATED/POST_MORTEM), we must validate key events
120
133
  current_status = incident.status
@@ -134,22 +147,31 @@ class ClosureReasonModal(IncidentSelectableModalMixin, SlackModal):
134
147
  }
135
148
  )
136
149
  return False
150
+ else:
151
+ # Early closure path - validate with the submitted closure reason
152
+ # Temporarily inject the submitted closure_reason so early-closure bypass applies
153
+ original_closure_reason = incident.closure_reason
154
+ incident.closure_reason = closure_reason
155
+ try:
156
+ can_close, reasons = incident.can_be_closed
157
+ finally:
158
+ incident.closure_reason = original_closure_reason
159
+ if not can_close:
160
+ # Build error message from reasons
161
+ error_messages = [reason[1] for reason in reasons]
162
+ error_text = "\n".join([f"• {msg}" for msg in error_messages])
163
+ ack(
164
+ response_action="errors",
165
+ errors={
166
+ "closure_message": f"Cannot close this incident:\n{error_text}"
167
+ },
168
+ )
169
+ return False
137
170
 
138
171
  # Clear ALL modals in the stack (not just this one)
139
172
  # This ensures the underlying "Update Status" modal is also closed
140
173
  ack(response_action="clear")
141
174
 
142
- # Extract form values
143
- state_values = body["view"]["state"]["values"]
144
- closure_reason = state_values["closure_reason"]["select_closure_reason"][
145
- "selected_option"
146
- ]["value"]
147
- closure_reference = (
148
- state_values["closure_reference"]["input_closure_reference"].get("value", "")
149
- or ""
150
- )
151
- message = state_values["closure_message"]["input_closure_message"]["value"]
152
-
153
175
  try:
154
176
  # Update incident with closure fields
155
177
  incident.closure_reason = closure_reason
@@ -165,9 +187,7 @@ class ClosureReasonModal(IncidentSelectableModalMixin, SlackModal):
165
187
  )
166
188
 
167
189
  except Exception:
168
- logger.exception(
169
- "Error closing incident #%s with reason", incident.id
170
- )
190
+ logger.exception("Error closing incident #%s with reason", incident.id)
171
191
  respond(
172
192
  body=body,
173
193
  text=f"❌ Failed to close incident #{incident.id}",
@@ -182,7 +202,11 @@ class ClosureReasonModal(IncidentSelectableModalMixin, SlackModal):
182
202
  f"✅ Incident #{incident.id} has been closed.\n"
183
203
  f"*Reason:* {ClosureReason(closure_reason).label}\n"
184
204
  f"*Message:* {message}"
185
- + (f"\n*Reference:* {closure_reference}" if closure_reference else "")
205
+ + (
206
+ f"\n*Reference:* {closure_reference}"
207
+ if closure_reference
208
+ else ""
209
+ )
186
210
  ),
187
211
  )
188
212
  except SlackApiError as e:
@@ -3,10 +3,15 @@ from __future__ import annotations
3
3
  import logging
4
4
  from typing import TYPE_CHECKING, Any
5
5
 
6
- from slack_sdk.models.blocks.blocks import Block, SectionBlock
6
+ from django.core.exceptions import ObjectDoesNotExist
7
+ from slack_sdk.models.blocks.block_elements import ButtonElement
8
+ from slack_sdk.models.blocks.blocks import ActionsBlock, Block, SectionBlock
7
9
  from slack_sdk.models.views import View
8
10
 
9
- from firefighter.slack.views.modals.base_modal.base import SlackModal
11
+ from firefighter.confluence.models import PostMortemManager
12
+ from firefighter.incidents.models.incident import Incident
13
+ from firefighter.slack.utils import respond
14
+ from firefighter.slack.views.modals.base_modal.base import SlackModal, app
10
15
  from firefighter.slack.views.modals.base_modal.mixins import (
11
16
  IncidentSelectableModalMixin,
12
17
  )
@@ -14,8 +19,6 @@ from firefighter.slack.views.modals.base_modal.mixins import (
14
19
  if TYPE_CHECKING:
15
20
  from slack_bolt.context.ack.ack import Ack
16
21
 
17
- from firefighter.incidents.models.incident import Incident
18
-
19
22
 
20
23
  logger = logging.getLogger(__name__)
21
24
 
@@ -32,8 +35,8 @@ class PostMortemModal(
32
35
  blocks: list[Block] = []
33
36
 
34
37
  # Check existing post-mortems
35
- has_confluence = hasattr(incident, "postmortem_for")
36
- has_jira = hasattr(incident, "jira_postmortem_for")
38
+ has_confluence = _safe_has_relation(incident, "postmortem_for")
39
+ has_jira = _safe_has_relation(incident, "jira_postmortem_for")
37
40
 
38
41
  if has_confluence or has_jira:
39
42
  blocks.append(
@@ -53,12 +56,29 @@ class PostMortemModal(
53
56
  text=f"• Jira: <{incident.jira_postmortem_for.issue_url}|{incident.jira_postmortem_for.jira_issue_key}>"
54
57
  )
55
58
  )
56
- else:
59
+ elif incident.needs_postmortem:
57
60
  blocks.append(
58
61
  SectionBlock(
59
62
  text=f"Post-mortem for incident #{incident.id} will be automatically created when the incident reaches MITIGATED status."
60
63
  )
61
64
  )
65
+ else:
66
+ blocks.extend(
67
+ [
68
+ SectionBlock(
69
+ text="P3 incident post-mortem is not mandatory. You can still have one if you think is necessary by clicking on the button below."
70
+ ),
71
+ ActionsBlock(
72
+ elements=[
73
+ ButtonElement(
74
+ text="Create post-mortem now",
75
+ action_id="incident_create_postmortem_now",
76
+ value=str(incident.id),
77
+ )
78
+ ]
79
+ ),
80
+ ]
81
+ )
62
82
 
63
83
  return View(
64
84
  type="modal",
@@ -76,4 +96,52 @@ class PostMortemModal(
76
96
  ack()
77
97
 
78
98
 
99
+ @app.action("incident_create_postmortem_now")
100
+ def handle_create_postmortem_action(ack: Ack, body: dict[str, Any]) -> None:
101
+ """Create post-mortem(s) on demand from the modal (e.g. P3+ incidents)."""
102
+ ack()
103
+
104
+ incident_id = str(body.get("actions", [{}])[0].get("value", "")).strip()
105
+ try:
106
+ incident = Incident.objects.get(pk=incident_id)
107
+ except Incident.DoesNotExist:
108
+ respond(body, text=":x: Incident not found.")
109
+ return
110
+
111
+ try:
112
+ confluence_pm, jira_pm = PostMortemManager.create_postmortem_for_incident(
113
+ incident
114
+ )
115
+ except Exception:
116
+ logger.exception("Failed to create post-mortem for incident #%s", incident_id)
117
+ respond(body, text=":x: Failed to create post-mortem. Please try again.")
118
+ return
119
+
120
+ created_targets: list[str] = []
121
+ if confluence_pm:
122
+ created_targets.append("Confluence")
123
+ if jira_pm:
124
+ created_targets.append("Jira")
125
+
126
+ if created_targets:
127
+ targets = " and ".join(created_targets)
128
+ respond(body, text=f":white_check_mark: {targets} post-mortem created.")
129
+ else:
130
+ respond(body, text=":warning: No post-mortem was created.")
131
+
132
+
79
133
  modal_postmortem = PostMortemModal()
134
+
135
+
136
+ def _safe_has_relation(instance: Incident, attr: str) -> bool:
137
+ """Safely check if a reverse relation exists without triggering KeyError in cache.
138
+
139
+ Django's reverse OneToOne descriptor can raise KeyError when using hasattr
140
+ on unsaved or freshly created instances. We guard against that here.
141
+ """
142
+ try:
143
+ getattr(instance, attr)
144
+ except (AttributeError, ObjectDoesNotExist, KeyError):
145
+ return False
146
+ else:
147
+ return True
@@ -127,12 +127,12 @@ class UpdateStatusModal(ModalForm[UpdateStatusFormSlack]):
127
127
  # Build error message from reasons
128
128
  error_messages = [reason[1] for reason in reasons]
129
129
  error_text = "\n".join([f"• {msg}" for msg in error_messages])
130
- logger.warning(f"Cannot close incident #{incident.id} via Update Status: {error_text}")
130
+ logger.warning(
131
+ f"Cannot close incident #{incident.id} via Update Status: {error_text}"
132
+ )
131
133
  ack(
132
134
  response_action="errors",
133
- errors={
134
- "status": f"Cannot close this incident:\n{error_text}"
135
- }
135
+ errors={"status": f"{error_text}"},
136
136
  )
137
137
  return
138
138
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firefighter-incident
3
- Version: 0.0.26
3
+ Version: 0.0.28
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=Fnf259POysYSI-aH6DhnxuHUxCVOFWsQLs4I5Ur_KMA,706
9
+ firefighter/_version.py,sha256=rsZxmANBVfE-nN63y-ZZ_71Lkm6YP4u4TgVEBJV3mNM,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
@@ -55,7 +55,7 @@ firefighter/confluence/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
55
55
  firefighter/confluence/admin.py,sha256=aDXghuuLc7G_TLt-655M31smx-H6vkIgLtEmmNCA3lg,1490
56
56
  firefighter/confluence/apps.py,sha256=vKswBwQL7L9e2JQwvRb7xy3myyE_GRldYX78jSY3XCM,406
57
57
  firefighter/confluence/client.py,sha256=xjSsrsGPF75JANNvam2YgiUkztuXhOcs9pMmPbb7ymk,6361
58
- firefighter/confluence/models.py,sha256=ExFip6Tw199jI5LVLqC8t4pOPsvQxfEhtQeWCqTijFw,8769
58
+ firefighter/confluence/models.py,sha256=66RNfD-lRdixZtOo5pNW2e-LZboTRjdoH2h-R7ne-q0,9154
59
59
  firefighter/confluence/serializers.py,sha256=CzuHVXIJNS47NCAJLXSTDOevtg5sf309XXEcWKQ1sAQ,258
60
60
  firefighter/confluence/service.py,sha256=dOQXj0uDInEm25nvL6lXiSH4hQ5oC2VDyBd1zbEcZ5U,12296
61
61
  firefighter/confluence/tables.py,sha256=ANEtFXzXyPK6E5FIrBC5XoQt5R3ZUY1DME_RbD1h_NE,732
@@ -147,6 +147,10 @@ firefighter/incidents/forms/update_key_events.py,sha256=1Xmnxe5OgZqLFS2HmMzQm3VG
147
147
  firefighter/incidents/forms/update_roles.py,sha256=Q26UPfwAj-8N23RNZLQkvmHGnS1_j_X5KQWjJmPjMKY,3635
148
148
  firefighter/incidents/forms/update_status.py,sha256=7GSno_EqD2Brd6wWcSb3zsP6nz8_mUTXXnl0QCRhv48,6682
149
149
  firefighter/incidents/forms/utils.py,sha256=15e_dBebVd9SvX03DYd0FyZ8s0YpxyBlZfIzEZattwg,4267
150
+ firefighter/incidents/management/__init__.py,sha256=A2LtnedT5NvTcNAN5nXMkPwK56JBNLuptcyObvq7zcc,40
151
+ firefighter/incidents/management/commands/__init__.py,sha256=wc5DFEklUo-wB-6VAAmsV5UTbo5s3t936Lu61z4lojs,29
152
+ firefighter/incidents/management/commands/backdate_incident_mitigated.py,sha256=phAXH18TNvzA03o1XtJfRVeOPbrKp8wdsBMx-QGAIeo,3410
153
+ firefighter/incidents/management/commands/test_postmortem_reminders.py,sha256=Bx-AVhkSjkL6c2_Eh7mRHa7qOEtytDl9T2hJYxcBC-4,4233
150
154
  firefighter/incidents/migrations/0001_initial_oss.py,sha256=OCrPbxf90h3NW9xolGGcsAryHKptD1TtKj5FucjBjg8,60311
151
155
  firefighter/incidents/migrations/0002_alter_severity_name_alter_user_password_featureteam.py,sha256=YfIJhw_-Yqm8qrkbp01461bkcUr7v5Zy90oHjkY3bSA,1113
152
156
  firefighter/incidents/migrations/0003_delete_featureteam.py,sha256=kH5UUSx3k5DtjR_goDxROdV0htCC2JZfBGwJpn-dEQs,336
@@ -176,12 +180,13 @@ firefighter/incidents/migrations/0026_alter_incidentcategory_options_and_more.py
176
180
  firefighter/incidents/migrations/0027_add_closure_fields.py,sha256=MDWckXmjJNC2iVoFJD6IIwDmmqyeL1VG_pHR568JAtk,1344
177
181
  firefighter/incidents/migrations/0028_add_closure_reason_constraint.py,sha256=z6FjCURDt9c-hyBeCvCKsbZOiuReYtbjtguIh3T6dnk,920
178
182
  firefighter/incidents/migrations/0029_add_custom_fields_to_incident.py,sha256=G6DsnP5bM4Hy0s8IqXhLYzFKt3eumEsCnJfPIw5tcX4,567
183
+ firefighter/incidents/migrations/0030_add_mitigated_at_field.py,sha256=pELNJWbAuctv_dA-pdD3fsqg4qMm5f6upW9hFrjNLDI,546
179
184
  firefighter/incidents/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
180
185
  firefighter/incidents/models/__init__.py,sha256=FLVyBwIdyxLdgSvXRAKC3fry9YwwqlqhitTIuG0vWrk,877
181
186
  firefighter/incidents/models/environment.py,sha256=51txwua3dCrWZ1iSG3ZA8rbDn9c00pyMAZujl9gwE5c,827
182
187
  firefighter/incidents/models/group.py,sha256=VrVL315VFUvKW69AZuRUBg1h0jZJvn8zWeMxMOWec1Y,700
183
188
  firefighter/incidents/models/impact.py,sha256=D9NngMtg4XdDWnMgdVYaWCoUZ-fMXTvfL0eTEk9sc7M,4854
184
- firefighter/incidents/models/incident.py,sha256=UdL9bc-3Ou-OS8P-nHO4YF6-Hk0mDkGNq1zjOq-TNFk,28022
189
+ firefighter/incidents/models/incident.py,sha256=4G4vLurh2bgMRo5eOuYgKWNRC8Xf_07cFf_UdpLPOOg,29638
185
190
  firefighter/incidents/models/incident_category.py,sha256=g4OHv_XQhWcH6dvkqkyCgjlruo_1eih_CdtAPgPhaW4,7744
186
191
  firefighter/incidents/models/incident_cost.py,sha256=juwOfJKRaNQpOHkRUCHShDDba0FU98YjRPkU4I0ofAU,1346
187
192
  firefighter/incidents/models/incident_cost_type.py,sha256=wm8diry_VySJzIjC9M3Yavv2tYbvJgpN9UDb2gFRuH4,845
@@ -273,7 +278,7 @@ firefighter/jira_app/admin.py,sha256=ZHAAbhy0hm_DcklK59KMmid_ZiPn8n5V6g7cZCSNrpc
273
278
  firefighter/jira_app/apps.py,sha256=T6vHrQuMZHJoTth-xjy3CbNfPv6DyXgcR3PSMju2JS4,504
274
279
  firefighter/jira_app/client.py,sha256=qpMqNTjJUq5OqAxmwvVOE20uJe7kp737HSdsiqUu1G4,21982
275
280
  firefighter/jira_app/models.py,sha256=2zKy5VaKkhiHYA8Dukz8g0NTG82Qy5UHAHY9eMv67NE,3097
276
- firefighter/jira_app/service_postmortem.py,sha256=7VQwtVGCVZaVGfRxQXL19IM4hHrgHmItUbrsyy2gZlM,10764
281
+ firefighter/jira_app/service_postmortem.py,sha256=tqJN91vZPX5ISd_PAGWDebHK225eZvDvwhi7ONy_D-A,11325
277
282
  firefighter/jira_app/types.py,sha256=Ukak1U1EhcH2jQPN-UoEL6AMZ-kzPsQ8c7FUr7GmahE,956
278
283
  firefighter/jira_app/utils.py,sha256=3xuzr8viZCBm6j2J9oFzA4bUvVW8TN1DOdlpbruJ_TE,3443
279
284
  firefighter/jira_app/management/__init__.py,sha256=wy4qMZb7_K-INwwGGEhMtEeI0XTLqgUw4P8_-VEnrEw,40
@@ -283,10 +288,10 @@ firefighter/jira_app/migrations/0002_add_jira_postmortem_model.py,sha256=oFSbYNc
283
288
  firefighter/jira_app/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
284
289
  firefighter/jira_app/signals/__init__.py,sha256=OpXFlbRgIrh73DGlUzQ6WUTedKsD5wYW9bxGMq_DnIs,325
285
290
  firefighter/jira_app/signals/incident_key_events_updated.py,sha256=uaV3MON1QzeOZizzAwSdyktBwe2mWxHJeNSvy9MYc3k,3204
286
- firefighter/jira_app/signals/postmortem_created.py,sha256=eOTQKcjspL-JHvXiCu-U1FzrqIr6OJ20GOK6gJ4dojw,5904
291
+ firefighter/jira_app/signals/postmortem_created.py,sha256=S7sKbEgo5RroC5ji1OepAT3HMckwS120dpKeUteNaXA,8300
287
292
  firefighter/jira_app/tasks/__init__.py,sha256=XLCPkolM6LwIUGv0MNbk_0lCuBHyzgRFHsE3vTRD5ds,86
288
293
  firefighter/jira_app/tasks/sync_users_jira.py,sha256=sSSLsVCdzkPNRS6Gt8j0YwCTuoRqkJAJLxDBu7IElmM,1437
289
- firefighter/jira_app/templates/jira/postmortem/impact.txt,sha256=eYlX0rytUiKZZIrWxMN23QXkLZ8JwlwPe5S2oiqZyFA,259
294
+ firefighter/jira_app/templates/jira/postmortem/impact.txt,sha256=WeF6uuwKjSER-CYqpLgH6p-wIkWgtVaeGKp8TsWSekQ,515
290
295
  firefighter/jira_app/templates/jira/postmortem/incident_summary.txt,sha256=Bnias41O8TR2v0CAWpOoyRVVBAyx6vk2iICeQsLOuCs,392
291
296
  firefighter/jira_app/templates/jira/postmortem/mitigation_actions.txt,sha256=7DWOcMhU0NiJugiUmEvSg1Z3ajW7IDZejprxt8urue0,242
292
297
  firefighter/jira_app/templates/jira/postmortem/root_causes.txt,sha256=27DgQdrtHvHcju1llyYqU1jufegeHjJN_qvVkyECINM,291
@@ -345,7 +350,7 @@ firefighter/slack/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
345
350
  firefighter/slack/admin.py,sha256=pNJbA-szxUUrghxv_Z0BNezu6lULDzFcOu_K5i4m7Cs,13963
346
351
  firefighter/slack/apps.py,sha256=gR0zWTtqT58tjPayBX22ZSzMkLiNpmoOvLShNvhJA6Q,664
347
352
  firefighter/slack/factories.py,sha256=tnrUTbtgehCuBr24MtTyJ3uezKC6gJbOdHuYZ5JBoyU,3886
348
- firefighter/slack/rules.py,sha256=PHbqi8sSTZi60TaahLfwdVr1B3GG_Rz1ldAT5q3XvG4,1651
353
+ firefighter/slack/rules.py,sha256=Y-DYJ_1D13a4nQNESCbhalNe2nC9xVNpYtSeWTgDcYc,2374
349
354
  firefighter/slack/slack_app.py,sha256=mvaH0hPFaNIUxEB7J0fy6y-PNPGsdPqjXFVmpTQ_hCo,4201
350
355
  firefighter/slack/slack_incident_context.py,sha256=PjE7-w-pGFyV4faw8EMsEFp4RG_T251RhofmqrsDG7Q,7277
351
356
  firefighter/slack/slack_templating.py,sha256=rWe8m1n648wizw08U_vLz8daRnp4zmkcWRqocIBpQj4,3841
@@ -359,7 +364,7 @@ firefighter/slack/management/commands/generate_manifest.py,sha256=zFWHAC7ioozcDd
359
364
  firefighter/slack/management/commands/switch_test_users.py,sha256=2KTSvCBxsEvZa61J8p0r3huPNhwuytcj2J7IawwZWpQ,11064
360
365
  firefighter/slack/messages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
361
366
  firefighter/slack/messages/base.py,sha256=biH-YEAaldJ-OLHEs5ZjW-gtUYUbjOqxrAEflqV2XS0,4593
362
- firefighter/slack/messages/slack_messages.py,sha256=xvkV_sEbBRkyqGigR_oQeC6Cr9KLWqqwxDXmPywyEok,35553
367
+ firefighter/slack/messages/slack_messages.py,sha256=9-zbxBuzDweKQZiwc2DMdiQcsnd23CGCi4YWhGgJx60,43713
363
368
  firefighter/slack/migrations/0001_initial_oss.py,sha256=XmTPgq7zCME2xDwzRFoVi4OegSIG9eSKoyTNoW05Qtg,12933
364
369
  firefighter/slack/migrations/0002_usergroup_tag.py,sha256=098tmGA81mT-R2uhb6uQfZ7gKiRG9bFhEwQ8rrp4SKM,583
365
370
  firefighter/slack/migrations/0003_alter_usergroup_tag.py,sha256=ncH3KUWEPZHlbdcAtOJ0KGt5H6EX-cKspTGU3osrAhE,591
@@ -368,6 +373,7 @@ firefighter/slack/migrations/0005_add_incident_categories_fields.py,sha256=KMdKf
368
373
  firefighter/slack/migrations/0006_copy_components_to_incident_categories.py,sha256=xUF7lLyWERux6SyIYHK2Uk1Yb4QLCGTaHW_KXVqX8n4,2478
369
374
  firefighter/slack/migrations/0007_remove_components_fields.py,sha256=_GXmcpB3enpVBT1NZ-tGDlh16r_cM-JkH2gebrmwIOs,563
370
375
  firefighter/slack/migrations/0008_alter_conversation_incident_categories_and_more.py,sha256=yzuAnunYvlF-wcYd7oe5h-kL5aOoawSXv_QGfmTFoBo,1034
376
+ firefighter/slack/migrations/0009_add_postmortem_reminder_periodic_task.py,sha256=Vze5TvhQExaF7-KymByzMZZE1wa0GHlWZXP1v1hub5w,2035
371
377
  firefighter/slack/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
372
378
  firefighter/slack/models/__init__.py,sha256=MGc4yuDnVhmAiHy1-5rjaLIfVv9JOup5arRutcUs8Ak,332
373
379
  firefighter/slack/models/conversation.py,sha256=f7a0muD0lrpf8mIhF6E2gEhNsgwZFw9jlKIQppZhNL0,16227
@@ -388,6 +394,7 @@ firefighter/slack/tasks/__init__.py,sha256=28QxZkakyi9l7Ae83fQuzOS-9EaBiwuh_peUZ
388
394
  firefighter/slack/tasks/fetch_conversations_members.py,sha256=lLQ491_l8HEJrjoDpD0AETqoFUFogkyMJ002hguA-Dg,5381
389
395
  firefighter/slack/tasks/reminder_postmortem.py,sha256=mZvT4cpzmMhC6JrWhZb1uFvTJJrbkEKgcCy6liKHrKM,2322
390
396
  firefighter/slack/tasks/send_message.py,sha256=N0FIE93bUnzbHdWkSWC-4-eLn737u6hHq8F8erMu8kI,810
397
+ firefighter/slack/tasks/send_postmortem_reminders.py,sha256=uyF9v8um2uxuYFyDSe2GhmG3iX0iAJrG1yE3LS0_NSo,4741
391
398
  firefighter/slack/tasks/send_reminders.py,sha256=hy1Q_rG2RUQdXNYEYiLyLnT7rkG8PFOmxur62YCCDrk,4370
392
399
  firefighter/slack/tasks/sync_users.py,sha256=T5ytYnZpcUqrh4sOklxWttsUk82C_2bwayg_fdcdg1g,2391
393
400
  firefighter/slack/tasks/update_usergroups_members.py,sha256=W-rPt3r2c9UboVMNiyQFvi_W7XWrR8ireVBWcCJMj5A,4642
@@ -410,20 +417,20 @@ firefighter/slack/views/events/message.py,sha256=c8tvo0btOUu_5Bc83oiO3IQbaEyoRiU
410
417
  firefighter/slack/views/events/message_deleted.py,sha256=tyA1-sAlG9ImcKIhqSn6EgujHmbvj4Uw2QzQ4JH4QwI,747
411
418
  firefighter/slack/views/events/reaction_added.py,sha256=AipwBnrU5B35D97YIZCXdSW8W7-9QTIIQqUcrLTLQ5c,4241
412
419
  firefighter/slack/views/modals/__init__.py,sha256=U9PapAIlpuYqBonOUmBGWT8_HjQa35ilMQJXGaFLgd0,1945
413
- firefighter/slack/views/modals/close.py,sha256=4j5iA-lmIFuCz7B9pgDmjxrqmfWFysqWEn1YIsE75zc,12161
414
- firefighter/slack/views/modals/closure_reason.py,sha256=N-gp0E6W8Z1d4aH1-8BxizNTlZ4syNIF4l5B9WZHFFA,7898
420
+ firefighter/slack/views/modals/close.py,sha256=eTT1IVEMXUrdUXsu5VSmC6-cGUsOYUGG7CN-3yAnuDo,17482
421
+ firefighter/slack/views/modals/closure_reason.py,sha256=qgjg6x5JBh5ggR6Z6aVhpnc3k8En38UxKtFb5RkTU4U,9016
415
422
  firefighter/slack/views/modals/downgrade_workflow.py,sha256=cRWsm3DmKRRI1-Jpjprb5xeY2U7HvRo6eZlUbGuzr1A,3192
416
423
  firefighter/slack/views/modals/edit.py,sha256=1N0OBSxsDuN6lJoH-djbEljy7f0LcDEpJF-U5YoEFXA,5895
417
424
  firefighter/slack/views/modals/key_event_message.py,sha256=C6yhQLQ6jBuhIr-YAoAyt-qZKu0V6nJMGZ_t3DLtUbo,5943
418
425
  firefighter/slack/views/modals/open.py,sha256=YIxpo8_C4cWCy_pQ3YRWl7NMyLmjqNjggTQINTBW6mo,29189
419
- firefighter/slack/views/modals/postmortem.py,sha256=7h_at4oMVFxN3IISbk1yzBDC46g4CFDsRnc873d8K-E,2451
426
+ firefighter/slack/views/modals/postmortem.py,sha256=Re4F0ZQEEOfdXljVCkswU1ESZksmpAqRy3es3_Wmeiw,5070
420
427
  firefighter/slack/views/modals/select.py,sha256=Y-Ji_ALnzhYkXDBAyi497UL1Xn2vCGqXCtj8eog75Jk,3312
421
428
  firefighter/slack/views/modals/send_sos.py,sha256=bP6HgYyDwPrIcTq7n_sQz6UQsxhYbvBDS4HjM0uRccA,4838
422
429
  firefighter/slack/views/modals/status.py,sha256=C8-eJRtquSeaHe568SC7yCFef1k14m2_6lUqBezdSH8,3970
423
430
  firefighter/slack/views/modals/trigger_oncall.py,sha256=h_LAD5X5rjMFWiDYTEp5VB9OaF7sTvKZhNaW3KQkw5M,5065
424
431
  firefighter/slack/views/modals/update.py,sha256=OF9sf-Z6IiviNmjN28MQNYiUbJ5tha0MdHUQyPpVFiY,2150
425
432
  firefighter/slack/views/modals/update_roles.py,sha256=De3Gv67MZQHyNdonX3S99F5MtKF_Rj3y71gdWibxBaM,2419
426
- firefighter/slack/views/modals/update_status.py,sha256=kJYcyruEwo7hok-z4Ge8HPxLMc-XPm-OrB7X_faNd7U,6000
433
+ firefighter/slack/views/modals/update_status.py,sha256=SYjQLXnWSZLk461b-L9WFRSxy7clIA4O0C4ZMrNWiuc,5964
427
434
  firefighter/slack/views/modals/utils.py,sha256=zKLJD2KhTGcX2d9WCYwshYRa6ok_9-ED1_pgOLp028s,2133
428
435
  firefighter/slack/views/modals/base_modal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
429
436
  firefighter/slack/views/modals/base_modal/base.py,sha256=7mvOxZTtegSmitSMnDvu8BK0qLUXoudUsda6CaLjdkY,12479
@@ -459,7 +466,7 @@ firefighter_tests/test_firefighter/test_logging.py,sha256=4HUH73vLDwmOCpMiXwDasM
459
466
  firefighter_tests/test_firefighter/test_sso.py,sha256=uX2ry0REDgXzQc9Y1BmAgI0OgbmzWoOv9H_GDyOqQmQ,5205
460
467
  firefighter_tests/test_firefighter/test_urls.py,sha256=UMGx4oW98RoL0ceePkIIKEVjbHdFECvQuGNXYAJForQ,4839
461
468
  firefighter_tests/test_incidents/test_enums.py,sha256=wMxxL1uakrmzJIi-2xkAvG-Y3NDDmIt0PHyOAJBz0yQ,4341
462
- firefighter_tests/test_incidents/test_incident_urls.py,sha256=j663qeBkv31aMlA0sLfzBOmp0iEv9OB-xvIwzrjlSZk,3793
469
+ firefighter_tests/test_incidents/test_incident_urls.py,sha256=VD9dj0IHHKXJHC5ApZg-L9CMtotaQb9uRAKcZIhOrDI,3978
463
470
  firefighter_tests/test_incidents/test_forms/conftest.py,sha256=YYF5Lm-Jmt-HM9zt_gjrNkiuqOaNMW8lLBr1crAP6J8,5423
464
471
  firefighter_tests/test_incidents/test_forms/test_closure_reason.py,sha256=H6RObqazFAit_pvo7N-lotiSsLOYMafZIk23A5Wiodg,3533
465
472
  firefighter_tests/test_incidents/test_forms/test_form_select_impact.py,sha256=DTaPGrJi8mXHfh7mhvDTKYVvDCxqarILauE59UDlwqo,3210
@@ -471,10 +478,10 @@ firefighter_tests/test_incidents/test_forms/test_update_key_events.py,sha256=rHR
471
478
  firefighter_tests/test_incidents/test_forms/test_update_status_workflow.py,sha256=q0xXU2BbBG8B0uvvyBWlo4HM8ckbcNAP05Fq8oJNtOw,16270
472
479
  firefighter_tests/test_incidents/test_forms/test_workflow_transitions.py,sha256=priKh7QYZxGDPu2SvPC8pGnqOsZWg5cLkyC40pDvLAU,7184
473
480
  firefighter_tests/test_incidents/test_models/test_incident_category.py,sha256=aRoBOhb8fNjLF9CMPZ1FXM8AT51Cd80XPsY2Y3wHY_M,5701
474
- firefighter_tests/test_incidents/test_models/test_incident_model.py,sha256=P5tmU5X4grt-yvoqjfuBkbd5tAWX3TptN-b_R3-1a9A,4719
481
+ firefighter_tests/test_incidents/test_models/test_incident_model.py,sha256=AWyWfQYcHNP9GPizIo0wRxNGTJTEJnAwNSd4UmRq-dk,8626
475
482
  firefighter_tests/test_incidents/test_models/test_migrations/test_incident_migrations.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
476
483
  firefighter_tests/test_incidents/test_utils/test_date_utils.py,sha256=ogP7qOEwItL4YGI5gbQPVssOS9ilwiuZC8OrT2qngBY,6568
477
- firefighter_tests/test_incidents/test_views/test_incident_detail_view.py,sha256=gKKFWIZVrD_P4p6DJjeHCW5uGXBUBVlCd95gJJYDpWQ,680
484
+ firefighter_tests/test_incidents/test_views/test_incident_detail_view.py,sha256=lkCIRfz99Ea0o0Id08LFWrjXLDmHv6XvezaSsjg-eYQ,871
478
485
  firefighter_tests/test_incidents/test_views/test_index_view.py,sha256=InpxbaWOFwRn4YWeIKZhj17vMymrQQf2p2LFhe2Bcdw,816
479
486
  firefighter_tests/test_jira_app/__init__.py,sha256=JxZ3v-0kiHOoO-N3kR8NHTmD8tEvuEYKW1GX_S1ZLMY,33
480
487
  firefighter_tests/test_jira_app/conftest.py,sha256=HmZd7EBZgng-rb3kIaB14TPVMixMG4YEvnShVqgjodE,545
@@ -502,24 +509,25 @@ firefighter_tests/test_slack/test_conversation_tags.py,sha256=nNqTZRRBfF6Z4wpFSY
502
509
  firefighter_tests/test_slack/test_signals_downgrade.py,sha256=mgl4H5vwr2kImf6g4IZbhv7YEPmMzbYSaVr8E6taL88,5420
503
510
  firefighter_tests/test_slack/test_slack_utils.py,sha256=9PLobMNXh3xDyFuwzcQFpKJhe4j__sIgf_WRHIpANJw,3957
504
511
  firefighter_tests/test_slack/messages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
505
- firefighter_tests/test_slack/messages/test_slack_messages.py,sha256=rs7zeBa_xsAyZIfXbOI1RA-zgViEPT5ZFNw6paBJvxc,17299
512
+ firefighter_tests/test_slack/messages/test_slack_messages.py,sha256=uyxfeAy1BQxx1zcCzlSJWn5YF1EnH-5Kt2XoIn9dekM,17484
506
513
  firefighter_tests/test_slack/test_models/test_conversations.py,sha256=t3ttmgwiu7c-N55iU3XZPmrkEhvkTzJoXszJncy4Bts,793
507
514
  firefighter_tests/test_slack/test_models/test_incident_channel.py,sha256=qWoGe9iadmK6-R8usWvjH87AHRkvhG_dHQeC3kHeJrs,17487
508
515
  firefighter_tests/test_slack/test_models/test_slack_user.py,sha256=uzur-Rf03I5dpUTO4ZI6O1arBUrAorg1Zvgshf8M-J4,7000
509
516
  firefighter_tests/test_slack/views/modals/conftest.py,sha256=TKJVQgqWaFs3Gg1T526pti9XpZBtQs47WBH6L_qSDeo,4532
510
- firefighter_tests/test_slack/views/modals/test_close.py,sha256=wuqMkpyUWKvC_gaTnJvRmdm8-wsmbLpCU_ith4ihsLY,45447
511
- firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py,sha256=hKfBv8fHfObmZc7OL6NUnxFueA1bowkNbu_-MF1anYI,5615
517
+ firefighter_tests/test_slack/views/modals/test_close.py,sha256=FWNV7RIUpqp3tiz9IBbBxaZk1XQt2f7vWB5TzJKYK3o,45630
518
+ firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py,sha256=mvg5RiCXQEp1GhyOBCNW4idkNR1StgZjPvFrjzJ549Q,8333
512
519
  firefighter_tests/test_slack/views/modals/test_edit.py,sha256=ykirry-S3i6PtoSs3rff_k6jqmvv1oMWC_iR8e5Jsg0,12022
513
520
  firefighter_tests/test_slack/views/modals/test_form_utils_multiple_choice.py,sha256=Svab_ZyYTMf0T-uJEQcm7gS1WzxtC4gPh1W--Z2v_Y8,8415
514
521
  firefighter_tests/test_slack/views/modals/test_key_event_message.py,sha256=BCg-c27ZLJqNgFuG4JDgXrSTp8_sT4FeBtpASzSq8NI,1107
515
522
  firefighter_tests/test_slack/views/modals/test_open.py,sha256=IzgG9le5NN_CvltehAIqkj94ioTKCqdA6yoRp2NlNsE,10700
516
523
  firefighter_tests/test_slack/views/modals/test_opening_unified.py,sha256=OejtLyc_mehav2TDaLzUnhilMNvhCzc6T4FodCqfQPk,17406
524
+ firefighter_tests/test_slack/views/modals/test_postmortem_modal.py,sha256=zNN40sIRSM5w_kyOcQ-AODkH5WpVxkSGVXkh9rMgmQ0,2378
517
525
  firefighter_tests/test_slack/views/modals/test_send_sos.py,sha256=_rE6jD-gOzcGyhlY0R9GzlGtPx65oOOguJYdENgxtLc,1289
518
526
  firefighter_tests/test_slack/views/modals/test_status.py,sha256=oQzPfwdg2tkbo9nfkO1GfS3WydxqSC6vy1AZjZDKT30,2226
519
- firefighter_tests/test_slack/views/modals/test_update_status.py,sha256=3ARHZPs22FTx7IjgOldzEpVxxWeHqEbe4kQphUuSp34,55928
527
+ firefighter_tests/test_slack/views/modals/test_update_status.py,sha256=vbHGx6dkM_0swE1vJ0HrkhI1oJzD_WHZuIQ-_arAxXo,55686
520
528
  firefighter_tests/test_slack/views/modals/test_utils.py,sha256=DJd2n9q6fFu8UuCRdiq9U_Cn19MdnC5c-ydLLrk6rkc,5218
521
- firefighter_incident-0.0.26.dist-info/METADATA,sha256=6maVl16hOTcZXnEYTy1PhaP4ligQTcK5QVZesD6C5a0,5570
522
- firefighter_incident-0.0.26.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
523
- firefighter_incident-0.0.26.dist-info/entry_points.txt,sha256=c13meJbv7YNmYz7MipMOQwzQ5IeFOPXUBYAJ44XMQsM,61
524
- firefighter_incident-0.0.26.dist-info/licenses/LICENSE,sha256=krRiGp-a9-1nH1bWpBEdxyTKLhjLmn6DMVVoIb0zF90,1087
525
- firefighter_incident-0.0.26.dist-info/RECORD,,
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,,
@@ -4,6 +4,7 @@ import logging
4
4
  from typing import TYPE_CHECKING
5
5
 
6
6
  import pytest
7
+ from django.apps import apps
7
8
  from django.urls import reverse
8
9
 
9
10
  from firefighter.incidents.factories import IncidentFactory, UserFactory
@@ -15,6 +16,9 @@ if TYPE_CHECKING:
15
16
 
16
17
  logger = logging.getLogger(__name__)
17
18
 
19
+ if not apps.is_installed("firefighter.confluence"):
20
+ pytest.skip("Confluence app not installed; skipping incident URLs tests", allow_module_level=True)
21
+
18
22
 
19
23
  @pytest.mark.django_db
20
24
  def test_incidents_dashboard_unauthorized(client: Client) -> None:
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
+ from unittest.mock import PropertyMock, patch
4
5
 
5
6
  import pytest
6
7
  from hypothesis import given
@@ -10,8 +11,11 @@ from hypothesis.strategies import builds
10
11
  from firefighter.incidents.enums import ClosureReason, IncidentStatus
11
12
  from firefighter.incidents.factories import IncidentFactory
12
13
  from firefighter.incidents.models import IncidentUpdate
14
+ from firefighter.jira_app.models import JiraPostMortem
13
15
 
14
16
  if TYPE_CHECKING:
17
+ from pytest_mock import MockerFixture
18
+
15
19
  from firefighter.incidents.models import Incident
16
20
 
17
21
 
@@ -58,7 +62,9 @@ class TestIncidentCanBeClosed:
58
62
  can_close, reasons = incident.can_be_closed
59
63
 
60
64
  # Should be closable (assuming no missing milestones)
61
- assert can_close is True or "STATUS_NOT_MITIGATED" not in [r[0] for r in reasons]
65
+ assert can_close is True or "STATUS_NOT_MITIGATED" not in [
66
+ r[0] for r in reasons
67
+ ]
62
68
 
63
69
  def test_can_close_incident_with_closure_reason(self) -> None:
64
70
  """Test that incidents with closure_reason can always be closed."""
@@ -73,6 +79,108 @@ class TestIncidentCanBeClosed:
73
79
  assert can_close is True
74
80
  assert reasons == []
75
81
 
82
+ def test_cannot_close_when_jira_postmortem_not_ready(self, settings: None) -> None:
83
+ """Block closure if Jira post-mortem exists but is not in Ready status."""
84
+ settings.ENABLE_JIRA_POSTMORTEM = True
85
+ incident = IncidentFactory.create(
86
+ _status=IncidentStatus.POST_MORTEM,
87
+ priority__value=1,
88
+ priority__needs_postmortem=True,
89
+ environment__value="PRD",
90
+ )
91
+ JiraPostMortem.objects.create(
92
+ incident=incident,
93
+ jira_issue_key="INC-999",
94
+ jira_issue_id="999",
95
+ created_by=incident.created_by,
96
+ )
97
+ incident.refresh_from_db()
98
+ assert hasattr(incident, "jira_postmortem_for")
99
+
100
+ with (
101
+ patch.object(
102
+ type(incident),
103
+ "needs_postmortem",
104
+ new_callable=PropertyMock,
105
+ return_value=True,
106
+ ),
107
+ patch.object(type(incident), "missing_milestones", return_value=[]),
108
+ patch(
109
+ "firefighter.jira_app.service_postmortem.jira_postmortem_service.is_postmortem_ready",
110
+ return_value=(False, "In Progress"),
111
+ ),
112
+ ):
113
+ can_close, reasons = incident.can_be_closed
114
+
115
+ assert can_close is False
116
+ assert any(r[0] == "POSTMORTEM_NOT_READY" for r in reasons)
117
+
118
+ def test_postmortem_ready_allows_closure(
119
+ self, mocker: MockerFixture, settings: None
120
+ ) -> None:
121
+ """When Jira PM is Ready, can_be_closed should allow closure for PM incidents."""
122
+ settings.ENABLE_JIRA_POSTMORTEM = True
123
+ incident = IncidentFactory.create(
124
+ _status=IncidentStatus.POST_MORTEM,
125
+ priority__value=1,
126
+ priority__needs_postmortem=True,
127
+ environment__value="PRD",
128
+ )
129
+ JiraPostMortem.objects.create(
130
+ incident=incident,
131
+ jira_issue_key="INC-READY",
132
+ jira_issue_id="123",
133
+ created_by=incident.created_by,
134
+ )
135
+
136
+ mocker.patch.object(type(incident), "missing_milestones", return_value=[])
137
+ mocker.patch(
138
+ "firefighter.jira_app.service_postmortem.jira_postmortem_service.is_postmortem_ready",
139
+ return_value=(True, "Ready"),
140
+ )
141
+
142
+ can_close, reasons = incident.can_be_closed
143
+
144
+ assert can_close is True
145
+ assert reasons == []
146
+
147
+ def test_postmortem_status_unknown_sets_reason(
148
+ self, mocker: MockerFixture, settings: None
149
+ ) -> None:
150
+ """Errors while checking Jira PM should return POSTMORTEM_STATUS_UNKNOWN."""
151
+ settings.ENABLE_JIRA_POSTMORTEM = True
152
+ incident = IncidentFactory.create(
153
+ _status=IncidentStatus.POST_MORTEM,
154
+ priority__value=1,
155
+ priority__needs_postmortem=True,
156
+ environment__value="PRD",
157
+ )
158
+ JiraPostMortem.objects.create(
159
+ incident=incident,
160
+ jira_issue_key="INC-ERR",
161
+ jira_issue_id="124",
162
+ created_by=incident.created_by,
163
+ )
164
+ incident.refresh_from_db()
165
+ assert hasattr(incident, "jira_postmortem_for")
166
+
167
+ mocker.patch.object(type(incident), "missing_milestones", return_value=[])
168
+ mocker.patch(
169
+ "firefighter.jira_app.service_postmortem.jira_postmortem_service.is_postmortem_ready",
170
+ side_effect=Exception("boom"),
171
+ )
172
+ mocker.patch.object(
173
+ type(incident),
174
+ "needs_postmortem",
175
+ new_callable=PropertyMock,
176
+ return_value=True,
177
+ )
178
+
179
+ can_close, reasons = incident.can_be_closed
180
+
181
+ assert can_close is False
182
+ assert any(r[0] == "POSTMORTEM_STATUS_UNKNOWN" for r in reasons)
183
+
76
184
 
77
185
  @pytest.mark.django_db
78
186
  class TestIncidentSetStatus:
@@ -1,12 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import pytest
4
+ from django.apps import apps
4
5
  from django.test import Client
5
6
  from django.urls import reverse
6
7
 
7
8
  from firefighter.incidents.models import Incident
8
9
  from firefighter.incidents.models.user import User
9
10
 
11
+ if not apps.is_installed("firefighter.confluence"):
12
+ pytest.skip("Confluence app not installed; skipping incident detail view test", allow_module_level=True)
13
+
10
14
 
11
15
  @pytest.mark.django_db
12
16
  def test_incident_detail_view(
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import pytest
4
+ from django.apps import apps
4
5
 
5
6
  from firefighter.incidents.enums import IncidentStatus
6
7
  from firefighter.incidents.factories import IncidentFactory, UserFactory
@@ -18,6 +19,9 @@ try:
18
19
  except (ImportError, AttributeError):
19
20
  PostMortem = None
20
21
 
22
+ if not apps.is_installed("firefighter.confluence"):
23
+ pytest.skip("Confluence app not installed; skipping slack message tests", allow_module_level=True)
24
+
21
25
 
22
26
  @pytest.mark.django_db
23
27
  class TestSlackMessageIncidentStatusUpdated:
@@ -5,6 +5,7 @@ from copy import deepcopy
5
5
  from unittest.mock import MagicMock, PropertyMock
6
6
 
7
7
  import pytest
8
+ from django.apps import apps
8
9
  from pytest_mock import MockerFixture
9
10
 
10
11
  from firefighter.incidents.enums import IncidentStatus
@@ -14,6 +15,9 @@ from firefighter.slack.views import CloseModal
14
15
 
15
16
  logger = logging.getLogger(__name__)
16
17
 
18
+ if not apps.is_installed("firefighter.confluence"):
19
+ pytest.skip("Confluence app not installed; skipping close modal tests", allow_module_level=True)
20
+
17
21
 
18
22
  @pytest.mark.django_db
19
23
  class TestCloseModal: