firefighter-incident 0.0.27__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.
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.27'
32
- __version_tuple__ = version_tuple = (0, 0, 27)
31
+ __version__ = version = '0.0.28'
32
+ __version_tuple__ = version_tuple = (0, 0, 28)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -1,12 +1,17 @@
1
1
  h2. Impact
2
2
 
3
+ h3. Business Impact
4
+
5
+ *{color:red}_TODO: Describe the business impact and precise the BV daily loss._{color}*
6
+ *{color:red}_For the BV daily loss, use [this documentation|https://manomano.atlassian.net/wiki/spaces/TC/pages/4089315451/How+to+calculate+BV+loss]._{color}*
7
+
8
+ h3. User Impact
9
+
10
+ *{color:red}_TODO: Describe the impact on users._{color}*
11
+
3
12
  h3. Affected Systems
4
13
  {% if components %}
5
14
  {% for component in components %}* {{ component.name }}
6
15
  {% endfor %}
7
16
  {% else %}_No components recorded._
8
17
  {% endif %}
9
-
10
- h3. User Impact
11
-
12
- *{color:red}_TODO: Describe the impact on users and business._{color}*
@@ -109,15 +109,36 @@ class SlackMessageIncidentPostMortemReminder(SlackMessageSurface):
109
109
  action_id=UpdateStatusModal.open_action,
110
110
  ),
111
111
  ),
112
- SectionBlock(
113
- text="2. Edit your post-mortem on Confluence",
114
- accessory=ButtonElement(
115
- text="Edit post-mortem",
116
- value=self.incident.postmortem_for.page_edit_url,
117
- url=self.incident.postmortem_for.page_edit_url,
118
- action_id="open_link",
119
- ),
120
- ),
112
+ ]
113
+
114
+ # Add post-mortem editing options based on available services
115
+ if hasattr(self.incident, "postmortem_for") and self.incident.postmortem_for:
116
+ blocks.append(
117
+ SectionBlock(
118
+ text="2. Edit your post-mortem on Confluence",
119
+ accessory=ButtonElement(
120
+ text="Edit post-mortem",
121
+ value=self.incident.postmortem_for.page_edit_url,
122
+ url=self.incident.postmortem_for.page_edit_url,
123
+ action_id="open_link",
124
+ ),
125
+ )
126
+ )
127
+ elif hasattr(self.incident, "jira_postmortem_for") and self.incident.jira_postmortem_for:
128
+ jira_pm = self.incident.jira_postmortem_for
129
+ blocks.append(
130
+ SectionBlock(
131
+ text="2. Edit your post-mortem on Jira",
132
+ accessory=ButtonElement(
133
+ text=f"Edit Jira post-mortem ({jira_pm.jira_issue_key})",
134
+ url=jira_pm.issue_url,
135
+ action_id="open_link",
136
+ ),
137
+ )
138
+ )
139
+
140
+ # Continue with remaining steps
141
+ blocks.extend([
121
142
  SectionBlock(
122
143
  text=f"3. Submit the key events to {APP_DISPLAY_NAME}",
123
144
  **accessory_kwargs,
@@ -138,7 +159,8 @@ class SlackMessageIncidentPostMortemReminder(SlackMessageSurface):
138
159
  )
139
160
  ]
140
161
  ),
141
- ]
162
+ ])
163
+
142
164
  if POSTMORTEM_HELP_URL:
143
165
  blocks.insert(
144
166
  4,
@@ -184,14 +206,30 @@ class SlackMessageIncidentFixedNextActions(SlackMessageSurface):
184
206
  ),
185
207
  ),
186
208
  DividerBlock(),
187
- ContextBlock(
188
- elements=[
189
- MarkdownTextObject(
190
- text="A post-mortem is *not* required for this incident.\nIf you want to create one, use `/incident postmortem` to create a new post-mortem page on Confluence."
191
- )
192
- ]
193
- ),
194
209
  ]
210
+
211
+ # Add post-mortem context message if any post-mortem service is enabled
212
+ enable_confluence = getattr(settings, "ENABLE_CONFLUENCE", False)
213
+ enable_jira_postmortem = getattr(settings, "ENABLE_JIRA_POSTMORTEM", False)
214
+
215
+ if enable_confluence or enable_jira_postmortem:
216
+ postmortem_text = "A post-mortem is *not* required for this incident.\nIf you want to create one, use `/incident postmortem` to create a new post-mortem page"
217
+
218
+ if enable_confluence and enable_jira_postmortem:
219
+ postmortem_text += " on Confluence or Jira."
220
+ elif enable_confluence:
221
+ postmortem_text += " on Confluence."
222
+ elif enable_jira_postmortem:
223
+ postmortem_text += " on Jira."
224
+
225
+ blocks.append(
226
+ ContextBlock(
227
+ elements=[
228
+ MarkdownTextObject(text=postmortem_text)
229
+ ]
230
+ )
231
+ )
232
+
195
233
  return blocks
196
234
 
197
235
 
@@ -612,7 +650,23 @@ class SlackMessageIncidentPostMortemCreated(SlackMessageSurface):
612
650
  return "\n".join(parts)
613
651
 
614
652
  def get_blocks(self) -> list[Block]:
615
- return [SectionBlock(text=self.get_text())]
653
+ blocks: list[Block] = [SectionBlock(text=self.get_text())]
654
+
655
+ # Add documentation link if Jira post-mortem exists
656
+ if hasattr(self.incident, "jira_postmortem_for") and self.incident.jira_postmortem_for:
657
+ blocks.append(
658
+ SectionBlock(
659
+ text="Need guidance on how to fill Post-Mortems in Jira? See our documentation",
660
+ accessory=ButtonElement(
661
+ text="Open documentation",
662
+ url="https://manomano.atlassian.net/wiki/spaces/TC/pages/5639635000/How+to+fill+Post-Mortems+in+Jira",
663
+ value="jira_postmortem_documentation",
664
+ action_id="open_link",
665
+ ),
666
+ )
667
+ )
668
+
669
+ return blocks
616
670
 
617
671
 
618
672
  class SlackMessageIncidentPostMortemCreatedAnnouncement(SlackMessageSurface):
@@ -696,7 +750,7 @@ class SlackMessagePostMortemReminder5Days(SlackMessageSurface):
696
750
  if hasattr(self.incident, "postmortem_for"):
697
751
  pm_links.append(
698
752
  ButtonElement(
699
- text="Open Confluence Post-Mortem",
753
+ text="Open Post-Mortem (Confluence)",
700
754
  url=self.incident.postmortem_for.page_edit_url,
701
755
  action_id="open_link",
702
756
  )
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firefighter-incident
3
- Version: 0.0.27
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=vq5GihUa4KrDz-Hix5Er9gtVMhfHNDBoLhzP1VsvNuE,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
@@ -291,7 +291,7 @@ firefighter/jira_app/signals/incident_key_events_updated.py,sha256=uaV3MON1QzeOZ
291
291
  firefighter/jira_app/signals/postmortem_created.py,sha256=S7sKbEgo5RroC5ji1OepAT3HMckwS120dpKeUteNaXA,8300
292
292
  firefighter/jira_app/tasks/__init__.py,sha256=XLCPkolM6LwIUGv0MNbk_0lCuBHyzgRFHsE3vTRD5ds,86
293
293
  firefighter/jira_app/tasks/sync_users_jira.py,sha256=sSSLsVCdzkPNRS6Gt8j0YwCTuoRqkJAJLxDBu7IElmM,1437
294
- 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
295
295
  firefighter/jira_app/templates/jira/postmortem/incident_summary.txt,sha256=Bnias41O8TR2v0CAWpOoyRVVBAyx6vk2iICeQsLOuCs,392
296
296
  firefighter/jira_app/templates/jira/postmortem/mitigation_actions.txt,sha256=7DWOcMhU0NiJugiUmEvSg1Z3ajW7IDZejprxt8urue0,242
297
297
  firefighter/jira_app/templates/jira/postmortem/root_causes.txt,sha256=27DgQdrtHvHcju1llyYqU1jufegeHjJN_qvVkyECINM,291
@@ -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=eCJUPSjzF_itXH9Z0VlfhloMNvjN3tsB1lmziO9Ksg0,41469
367
+ firefighter/slack/messages/slack_messages.py,sha256=9-zbxBuzDweKQZiwc2DMdiQcsnd23CGCi4YWhGgJx60,43713
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
@@ -423,7 +423,7 @@ firefighter/slack/views/modals/downgrade_workflow.py,sha256=cRWsm3DmKRRI1-Jpjprb
423
423
  firefighter/slack/views/modals/edit.py,sha256=1N0OBSxsDuN6lJoH-djbEljy7f0LcDEpJF-U5YoEFXA,5895
424
424
  firefighter/slack/views/modals/key_event_message.py,sha256=C6yhQLQ6jBuhIr-YAoAyt-qZKu0V6nJMGZ_t3DLtUbo,5943
425
425
  firefighter/slack/views/modals/open.py,sha256=YIxpo8_C4cWCy_pQ3YRWl7NMyLmjqNjggTQINTBW6mo,29189
426
- firefighter/slack/views/modals/postmortem.py,sha256=7h_at4oMVFxN3IISbk1yzBDC46g4CFDsRnc873d8K-E,2451
426
+ firefighter/slack/views/modals/postmortem.py,sha256=Re4F0ZQEEOfdXljVCkswU1ESZksmpAqRy3es3_Wmeiw,5070
427
427
  firefighter/slack/views/modals/select.py,sha256=Y-Ji_ALnzhYkXDBAyi497UL1Xn2vCGqXCtj8eog75Jk,3312
428
428
  firefighter/slack/views/modals/send_sos.py,sha256=bP6HgYyDwPrIcTq7n_sQz6UQsxhYbvBDS4HjM0uRccA,4838
429
429
  firefighter/slack/views/modals/status.py,sha256=C8-eJRtquSeaHe568SC7yCFef1k14m2_6lUqBezdSH8,3970
@@ -521,12 +521,13 @@ firefighter_tests/test_slack/views/modals/test_form_utils_multiple_choice.py,sha
521
521
  firefighter_tests/test_slack/views/modals/test_key_event_message.py,sha256=BCg-c27ZLJqNgFuG4JDgXrSTp8_sT4FeBtpASzSq8NI,1107
522
522
  firefighter_tests/test_slack/views/modals/test_open.py,sha256=IzgG9le5NN_CvltehAIqkj94ioTKCqdA6yoRp2NlNsE,10700
523
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
524
525
  firefighter_tests/test_slack/views/modals/test_send_sos.py,sha256=_rE6jD-gOzcGyhlY0R9GzlGtPx65oOOguJYdENgxtLc,1289
525
526
  firefighter_tests/test_slack/views/modals/test_status.py,sha256=oQzPfwdg2tkbo9nfkO1GfS3WydxqSC6vy1AZjZDKT30,2226
526
527
  firefighter_tests/test_slack/views/modals/test_update_status.py,sha256=vbHGx6dkM_0swE1vJ0HrkhI1oJzD_WHZuIQ-_arAxXo,55686
527
528
  firefighter_tests/test_slack/views/modals/test_utils.py,sha256=DJd2n9q6fFu8UuCRdiq9U_Cn19MdnC5c-ydLLrk6rkc,5218
528
- firefighter_incident-0.0.27.dist-info/METADATA,sha256=lHCUnR9PYiRLl_zG_MfvG23UsE_FZWbgQ4zlRdeIilY,5570
529
- firefighter_incident-0.0.27.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
530
- firefighter_incident-0.0.27.dist-info/entry_points.txt,sha256=c13meJbv7YNmYz7MipMOQwzQ5IeFOPXUBYAJ44XMQsM,61
531
- firefighter_incident-0.0.27.dist-info/licenses/LICENSE,sha256=krRiGp-a9-1nH1bWpBEdxyTKLhjLmn6DMVVoIb0zF90,1087
532
- firefighter_incident-0.0.27.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,,
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ from types import SimpleNamespace
4
+
5
+ from firefighter.slack.views.modals.postmortem import PostMortemModal
6
+
7
+
8
+ class TestPostMortemModal:
9
+ @staticmethod
10
+ def _get_text_blocks(view_dict: dict) -> list[str]:
11
+ return [
12
+ block.get("text", {}).get("text", "")
13
+ for block in view_dict.get("blocks", [])
14
+ if block.get("type") == "section"
15
+ ]
16
+
17
+ @staticmethod
18
+ def _get_action_elements(view_dict: dict) -> list[dict]:
19
+ elements: list[dict] = []
20
+ for block in view_dict.get("blocks", []):
21
+ if block.get("type") == "actions":
22
+ elements.extend(block.get("elements", []))
23
+ return elements
24
+
25
+ @staticmethod
26
+ def test_p1_p2_shows_auto_creation_message(mocker):
27
+ incident = SimpleNamespace(id=123, needs_postmortem=True)
28
+
29
+ # No existing PMs
30
+ mocker.patch(
31
+ "firefighter.slack.views.modals.postmortem._safe_has_relation",
32
+ return_value=False,
33
+ create=True,
34
+ )
35
+
36
+ view = PostMortemModal().build_modal_fn(incident)
37
+ view_dict = view.to_dict()
38
+
39
+ texts = TestPostMortemModal._get_text_blocks(view_dict)
40
+ assert any(
41
+ "automatically created when the incident reaches MITIGATED" in t
42
+ for t in texts
43
+ )
44
+ # No manual-create button for mandatory PMs
45
+ action_ids = [
46
+ el.get("action_id")
47
+ for el in TestPostMortemModal._get_action_elements(view_dict)
48
+ ]
49
+ assert "incident_create_postmortem_now" not in action_ids
50
+
51
+ @staticmethod
52
+ def test_p3_shows_optional_message_and_button(mocker):
53
+ incident = SimpleNamespace(id=456, needs_postmortem=False)
54
+
55
+ # No existing PMs
56
+ mocker.patch(
57
+ "firefighter.slack.views.modals.postmortem._safe_has_relation",
58
+ return_value=False,
59
+ create=True,
60
+ )
61
+
62
+ view = PostMortemModal().build_modal_fn(incident)
63
+ view_dict = view.to_dict()
64
+
65
+ texts = TestPostMortemModal._get_text_blocks(view_dict)
66
+ assert any("P3 incident post-mortem is not mandatory" in t for t in texts)
67
+
68
+ action_ids = [
69
+ el.get("action_id")
70
+ for el in TestPostMortemModal._get_action_elements(view_dict)
71
+ ]
72
+ assert "incident_create_postmortem_now" in action_ids