firefighter-incident 0.0.35__py3-none-any.whl → 0.0.37__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- firefighter/_version.py +2 -2
- firefighter/incidents/models/incident.py +25 -13
- firefighter/incidents/models/incident_update.py +2 -8
- firefighter/incidents/static/css/main.min.css +1 -1
- firefighter/incidents/templates/pages/incident_detail.html +15 -2
- firefighter/incidents/views/views.py +28 -0
- firefighter/raid/client.py +5 -0
- firefighter/raid/serializers.py +311 -26
- firefighter/raid/signals/incident_updated.py +332 -17
- firefighter/raid/utils.py +13 -3
- {firefighter_incident-0.0.35.dist-info → firefighter_incident-0.0.37.dist-info}/METADATA +1 -1
- {firefighter_incident-0.0.35.dist-info → firefighter_incident-0.0.37.dist-info}/RECORD +20 -18
- firefighter_tests/test_incidents/test_views/test_incident_detail_jira_postmortem.py +63 -0
- firefighter_tests/test_raid/test_jira_status_sync.py +209 -0
- firefighter_tests/test_raid/test_raid_serializers.py +106 -46
- firefighter_tests/test_raid/test_raid_signals.py +43 -31
- firefighter_tests/test_slack/views/modals/test_update_status.py +28 -0
- {firefighter_incident-0.0.35.dist-info → firefighter_incident-0.0.37.dist-info}/WHEEL +0 -0
- {firefighter_incident-0.0.35.dist-info → firefighter_incident-0.0.37.dist-info}/entry_points.txt +0 -0
- {firefighter_incident-0.0.35.dist-info → firefighter_incident-0.0.37.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,17 +3,46 @@ from __future__ import annotations
|
|
|
3
3
|
import logging
|
|
4
4
|
from typing import TYPE_CHECKING, Any
|
|
5
5
|
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
from django.core.cache import cache
|
|
8
|
+
from django.db.models.signals import post_save
|
|
6
9
|
from django.dispatch.dispatcher import receiver
|
|
7
10
|
|
|
8
11
|
from firefighter.incidents.enums import IncidentStatus
|
|
12
|
+
from firefighter.incidents.models.incident import Incident
|
|
9
13
|
from firefighter.incidents.signals import incident_updated
|
|
10
|
-
from firefighter.raid.client import client
|
|
14
|
+
from firefighter.raid.client import RAID_JIRA_WORKFLOW_NAME, client
|
|
15
|
+
from firefighter.raid.utils import normalize_cache_value
|
|
11
16
|
|
|
12
17
|
if TYPE_CHECKING:
|
|
13
|
-
from firefighter.incidents.models.incident import Incident
|
|
14
18
|
from firefighter.incidents.models.incident_update import IncidentUpdate
|
|
15
19
|
|
|
16
20
|
logger = logging.getLogger(__name__)
|
|
21
|
+
JIRA_SYNC_CACHE_TIMEOUT = getattr(settings, "JIRA_SYNC_CACHE_TIMEOUT", 60)
|
|
22
|
+
|
|
23
|
+
JIRA_STATUS_INCOMING = "Incoming"
|
|
24
|
+
JIRA_STATUS_PENDING_RESOLUTION = "Pending resolution"
|
|
25
|
+
JIRA_STATUS_IN_PROGRESS = "in progress"
|
|
26
|
+
JIRA_STATUS_REPORTER_VALIDATION = "Reporter validation"
|
|
27
|
+
JIRA_STATUS_CLOSED = "Closed"
|
|
28
|
+
|
|
29
|
+
IMPACT_TO_JIRA_STATUS_MAP: dict[IncidentStatus, str] = {
|
|
30
|
+
IncidentStatus.OPEN: JIRA_STATUS_INCOMING,
|
|
31
|
+
IncidentStatus.INVESTIGATING: JIRA_STATUS_IN_PROGRESS,
|
|
32
|
+
IncidentStatus.MITIGATING: JIRA_STATUS_IN_PROGRESS,
|
|
33
|
+
IncidentStatus.MITIGATED: JIRA_STATUS_REPORTER_VALIDATION,
|
|
34
|
+
IncidentStatus.POST_MORTEM: JIRA_STATUS_REPORTER_VALIDATION,
|
|
35
|
+
IncidentStatus.CLOSED: JIRA_STATUS_CLOSED,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _set_impact_to_jira_cache(
|
|
40
|
+
incident_id: Any, field: str, value: Any, timeout: int = JIRA_SYNC_CACHE_TIMEOUT
|
|
41
|
+
) -> None:
|
|
42
|
+
cache_key = (
|
|
43
|
+
f"sync:impact_to_jira:{incident_id}:{field}:{normalize_cache_value(value)}"
|
|
44
|
+
)
|
|
45
|
+
cache.set(cache_key, value=True, timeout=timeout)
|
|
17
46
|
|
|
18
47
|
|
|
19
48
|
@receiver(signal=incident_updated, sender="update_status")
|
|
@@ -31,29 +60,315 @@ def incident_updated_close_ticket_when_mitigated_or_postmortem(
|
|
|
31
60
|
- P3+ (no postmortem): Close when incident is MITIGATED or CLOSED
|
|
32
61
|
- POST_MORTEM status never closes the ticket (it remains open during PM phase)
|
|
33
62
|
"""
|
|
63
|
+
logger.debug(
|
|
64
|
+
"incident_updated handler invoked for incident #%s with status %s; updated_fields=%s event_type=%s",
|
|
65
|
+
getattr(incident, "id", "unknown"),
|
|
66
|
+
incident_update.status,
|
|
67
|
+
updated_fields,
|
|
68
|
+
incident_update.event_type,
|
|
69
|
+
)
|
|
70
|
+
# Skip if this update was produced by Jira webhook sync to avoid redundant close calls
|
|
71
|
+
if incident_update.event_type == "jira_status_sync":
|
|
72
|
+
logger.debug(
|
|
73
|
+
"Skipping Jira transition: incident #%s update came from Jira (event_type=jira_status_sync)",
|
|
74
|
+
getattr(incident, "id", "unknown"),
|
|
75
|
+
)
|
|
76
|
+
return
|
|
77
|
+
|
|
34
78
|
if "_status" not in updated_fields:
|
|
79
|
+
logger.debug(
|
|
80
|
+
"Skipping Jira transition: incident #%s update lacks _status in updated_fields (%s)",
|
|
81
|
+
getattr(incident, "id", "unknown"),
|
|
82
|
+
updated_fields,
|
|
83
|
+
)
|
|
35
84
|
return
|
|
36
85
|
|
|
37
86
|
if not hasattr(incident, "jira_ticket") or incident.jira_ticket is None:
|
|
38
87
|
logger.warning(
|
|
39
|
-
|
|
88
|
+
"Trying to close Jira ticket for incident %s but no Jira ticket found",
|
|
89
|
+
getattr(incident, "id", "unknown"),
|
|
90
|
+
)
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
# Special case: when Impact moves to MITIGATING, Jira must go through two steps:
|
|
94
|
+
# "Pending resolution" then "in progress".
|
|
95
|
+
if incident_update.status == IncidentStatus.MITIGATING:
|
|
96
|
+
all_steps_succeeded = True
|
|
97
|
+
for step in (JIRA_STATUS_PENDING_RESOLUTION, JIRA_STATUS_IN_PROGRESS):
|
|
98
|
+
try:
|
|
99
|
+
logger.debug(
|
|
100
|
+
"Transitioning Jira ticket %s via workflow %s to status %s (incident #%s, impact status %s)",
|
|
101
|
+
incident.jira_ticket.id,
|
|
102
|
+
RAID_JIRA_WORKFLOW_NAME,
|
|
103
|
+
step,
|
|
104
|
+
getattr(incident, "id", "unknown"),
|
|
105
|
+
incident_update.status,
|
|
106
|
+
)
|
|
107
|
+
client.transition_issue_auto(
|
|
108
|
+
incident.jira_ticket.id, step, RAID_JIRA_WORKFLOW_NAME
|
|
109
|
+
)
|
|
110
|
+
except Exception:
|
|
111
|
+
all_steps_succeeded = False
|
|
112
|
+
logger.exception(
|
|
113
|
+
"Failed to transition Jira ticket %s to %s for incident %s",
|
|
114
|
+
incident.jira_ticket.id,
|
|
115
|
+
step,
|
|
116
|
+
getattr(incident, "id", "unknown"),
|
|
117
|
+
)
|
|
118
|
+
if all_steps_succeeded:
|
|
119
|
+
logger.info(
|
|
120
|
+
"Transitioned Jira ticket %s through Pending resolution -> in progress from Impact status %s",
|
|
121
|
+
incident.jira_ticket.id,
|
|
122
|
+
incident_update.status.label if incident_update.status else "Unknown",
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
logger.warning(
|
|
126
|
+
"At least one Jira transition failed while moving ticket %s to MITIGATING (incident #%s)",
|
|
127
|
+
incident.jira_ticket.id,
|
|
128
|
+
getattr(incident, "id", "unknown"),
|
|
129
|
+
)
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
# Decide target Jira status based on Impact status and postmortem requirement.
|
|
133
|
+
# P3+ (no postmortem): close Jira when Impact reaches MITIGATED or CLOSED.
|
|
134
|
+
# P1/P2 (needs_postmortem): close Jira only when Impact reaches CLOSED.
|
|
135
|
+
incident_status = incident_update.status
|
|
136
|
+
if incident_status is None:
|
|
137
|
+
logger.info(
|
|
138
|
+
"Skipping Jira transition: incident #%s status is None",
|
|
139
|
+
getattr(incident, "id", "unknown"),
|
|
140
|
+
)
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
target_jira_status: str | None = (
|
|
144
|
+
JIRA_STATUS_CLOSED
|
|
145
|
+
if incident_status == IncidentStatus.CLOSED
|
|
146
|
+
else IMPACT_TO_JIRA_STATUS_MAP.get(incident_status)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if target_jira_status is None:
|
|
150
|
+
logger.info(
|
|
151
|
+
"Skipping Jira transition: no Jira status mapping for Impact status %s (incident #%s)",
|
|
152
|
+
incident_update.status,
|
|
153
|
+
getattr(incident, "id", "unknown"),
|
|
154
|
+
)
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
incident_id = getattr(incident, "id", None)
|
|
159
|
+
if incident_id is not None:
|
|
160
|
+
_set_impact_to_jira_cache(incident_id, "status", target_jira_status)
|
|
161
|
+
logger.debug(
|
|
162
|
+
"Transitioning Jira ticket %s via workflow %s to status %s (incident #%s, impact status %s)",
|
|
163
|
+
incident.jira_ticket.id,
|
|
164
|
+
RAID_JIRA_WORKFLOW_NAME,
|
|
165
|
+
target_jira_status,
|
|
166
|
+
getattr(incident, "id", "unknown"),
|
|
167
|
+
incident_update.status,
|
|
168
|
+
)
|
|
169
|
+
client.transition_issue_auto(
|
|
170
|
+
incident.jira_ticket.id, target_jira_status, RAID_JIRA_WORKFLOW_NAME
|
|
171
|
+
)
|
|
172
|
+
logger.info(
|
|
173
|
+
"Transitioned Jira ticket %s to %s from Impact status %s",
|
|
174
|
+
incident.jira_ticket.id,
|
|
175
|
+
target_jira_status,
|
|
176
|
+
incident_update.status.label if incident_update.status else "Unknown",
|
|
177
|
+
)
|
|
178
|
+
except Exception:
|
|
179
|
+
logger.exception(
|
|
180
|
+
"Failed to transition Jira ticket %s to %s for incident %s",
|
|
181
|
+
incident.jira_ticket.id,
|
|
182
|
+
target_jira_status,
|
|
183
|
+
getattr(incident, "id", "unknown"),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# Listen to all incident_updated signals so both UI (update_status) and API/admin paths trigger
|
|
188
|
+
@receiver(signal=incident_updated)
|
|
189
|
+
def incident_updated_sync_priority_to_jira(
|
|
190
|
+
sender: Any,
|
|
191
|
+
incident: Incident,
|
|
192
|
+
incident_update: IncidentUpdate,
|
|
193
|
+
updated_fields: list[str],
|
|
194
|
+
**kwargs: Any,
|
|
195
|
+
) -> None:
|
|
196
|
+
"""Push Impact priority changes to Jira custom priority field (customfield_11064).
|
|
197
|
+
Skips if change originated from Jira (event_type='jira_priority_sync') to avoid loops.
|
|
198
|
+
"""
|
|
199
|
+
logger.debug(
|
|
200
|
+
"Priority sync handler invoked: incident #%s sender=%s updated_fields=%s event_type=%s",
|
|
201
|
+
getattr(incident, "id", "unknown"),
|
|
202
|
+
sender,
|
|
203
|
+
updated_fields,
|
|
204
|
+
incident_update.event_type,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if incident_update.event_type == "jira_priority_sync":
|
|
208
|
+
logger.debug(
|
|
209
|
+
"Skipping Jira priority sync: incident #%s update came from Jira (event_type=jira_priority_sync)",
|
|
210
|
+
getattr(incident, "id", "unknown"),
|
|
211
|
+
)
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
if "priority_id" not in updated_fields:
|
|
215
|
+
logger.debug(
|
|
216
|
+
"Skipping Jira priority sync: incident #%s update lacks priority_id in updated_fields (%s) sender=%s",
|
|
217
|
+
getattr(incident, "id", "unknown"),
|
|
218
|
+
updated_fields,
|
|
219
|
+
sender,
|
|
220
|
+
)
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
if not hasattr(incident, "jira_ticket") or incident.jira_ticket is None:
|
|
224
|
+
logger.debug(
|
|
225
|
+
"Skipping Jira priority sync: incident #%s has no Jira ticket",
|
|
226
|
+
getattr(incident, "id", "unknown"),
|
|
227
|
+
)
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
if not incident.priority:
|
|
231
|
+
logger.debug(
|
|
232
|
+
"Skipping Jira priority sync: incident #%s priority is missing",
|
|
233
|
+
getattr(incident, "id", "unknown"),
|
|
40
234
|
)
|
|
41
235
|
return
|
|
42
236
|
|
|
43
|
-
|
|
44
|
-
|
|
237
|
+
try:
|
|
238
|
+
incident_id = getattr(incident, "id", None)
|
|
239
|
+
if incident_id is not None:
|
|
240
|
+
_set_impact_to_jira_cache(incident_id, "priority", incident.priority.value)
|
|
241
|
+
client.update_issue_fields(
|
|
242
|
+
incident.jira_ticket.id,
|
|
243
|
+
customfield_11064={"value": str(incident.priority.value)},
|
|
244
|
+
)
|
|
245
|
+
logger.info(
|
|
246
|
+
"Synced priority %s to Jira ticket %s (customfield_11064) for incident #%s",
|
|
247
|
+
incident.priority.value,
|
|
248
|
+
incident.jira_ticket.id,
|
|
249
|
+
getattr(incident, "id", "unknown"),
|
|
250
|
+
)
|
|
251
|
+
except Exception:
|
|
252
|
+
logger.exception(
|
|
253
|
+
"Failed to sync priority %s to Jira ticket %s for incident %s",
|
|
254
|
+
incident.priority.value,
|
|
255
|
+
incident.jira_ticket.id,
|
|
256
|
+
getattr(incident, "id", "unknown"),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# Fallback: if an Incident save bypasses incident_updated (e.g., admin inline), push priority anyway.
|
|
261
|
+
@receiver(post_save, sender=Incident)
|
|
262
|
+
def incident_priority_post_save_fallback(
|
|
263
|
+
sender: Any,
|
|
264
|
+
instance: Incident,
|
|
265
|
+
*,
|
|
266
|
+
created: bool,
|
|
267
|
+
update_fields: set[str] | None,
|
|
268
|
+
**kwargs: Any,
|
|
269
|
+
) -> None:
|
|
270
|
+
"""Fallback to push priority to Jira when Incident saves with priority_id in update_fields
|
|
271
|
+
but no incident_updated signal fired (e.g., admin edits). Skips when marked to avoid loops.
|
|
272
|
+
"""
|
|
273
|
+
if created:
|
|
274
|
+
return
|
|
275
|
+
if update_fields and "priority_id" not in update_fields:
|
|
276
|
+
return
|
|
277
|
+
if getattr(instance, "_skip_priority_sync", False):
|
|
278
|
+
logger.debug(
|
|
279
|
+
"Skipping post_save priority sync for incident #%s due to skip flag",
|
|
280
|
+
getattr(instance, "id", "unknown"),
|
|
281
|
+
)
|
|
282
|
+
return
|
|
283
|
+
if not hasattr(instance, "jira_ticket") or instance.jira_ticket is None:
|
|
284
|
+
logger.debug(
|
|
285
|
+
"Skipping post_save priority sync: incident #%s has no Jira ticket",
|
|
286
|
+
getattr(instance, "id", "unknown"),
|
|
287
|
+
)
|
|
288
|
+
return
|
|
289
|
+
if not instance.priority:
|
|
290
|
+
logger.debug(
|
|
291
|
+
"Skipping post_save priority sync: incident #%s priority missing",
|
|
292
|
+
getattr(instance, "id", "unknown"),
|
|
293
|
+
)
|
|
294
|
+
return
|
|
45
295
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
296
|
+
try:
|
|
297
|
+
_set_impact_to_jira_cache(instance.id, "priority", instance.priority.value)
|
|
298
|
+
client.update_issue_fields(
|
|
299
|
+
instance.jira_ticket.id,
|
|
300
|
+
customfield_11064={"value": str(instance.priority.value)},
|
|
301
|
+
)
|
|
302
|
+
logger.info(
|
|
303
|
+
"Post-save synced priority %s to Jira ticket %s (customfield_11064) for incident #%s",
|
|
304
|
+
instance.priority.value,
|
|
305
|
+
instance.jira_ticket.id,
|
|
306
|
+
getattr(instance, "id", "unknown"),
|
|
307
|
+
)
|
|
308
|
+
except Exception:
|
|
309
|
+
logger.exception(
|
|
310
|
+
"Failed post-save priority sync %s to Jira ticket %s for incident %s",
|
|
311
|
+
instance.priority.value,
|
|
312
|
+
instance.jira_ticket.id,
|
|
313
|
+
getattr(instance, "id", "unknown"),
|
|
314
|
+
)
|
|
52
315
|
|
|
53
|
-
# POST_MORTEM status never closes the ticket - it stays open during PM phase
|
|
54
316
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
317
|
+
@receiver(post_save, sender=Incident)
|
|
318
|
+
def incident_status_post_save_fallback(
|
|
319
|
+
sender: Any,
|
|
320
|
+
instance: Incident,
|
|
321
|
+
*,
|
|
322
|
+
created: bool,
|
|
323
|
+
update_fields: set[str] | None,
|
|
324
|
+
**kwargs: Any,
|
|
325
|
+
) -> None:
|
|
326
|
+
"""Fallback to push status to Jira when Incident saves with status in update_fields
|
|
327
|
+
but no incident_updated signal fired (e.g., admin edits). Skips when marked to avoid loops.
|
|
328
|
+
"""
|
|
329
|
+
if created:
|
|
330
|
+
return
|
|
331
|
+
if (
|
|
332
|
+
update_fields
|
|
333
|
+
and "_status" not in update_fields
|
|
334
|
+
and "status" not in update_fields
|
|
335
|
+
):
|
|
336
|
+
return
|
|
337
|
+
if getattr(instance, "_skip_status_sync", False):
|
|
338
|
+
logger.debug(
|
|
339
|
+
"Skipping post_save status sync for incident #%s due to skip flag",
|
|
340
|
+
getattr(instance, "id", "unknown"),
|
|
341
|
+
)
|
|
342
|
+
return
|
|
343
|
+
if not hasattr(instance, "jira_ticket") or instance.jira_ticket is None:
|
|
344
|
+
logger.debug(
|
|
345
|
+
"Skipping post_save status sync: incident #%s has no Jira ticket",
|
|
346
|
+
getattr(instance, "id", "unknown"),
|
|
347
|
+
)
|
|
348
|
+
return
|
|
349
|
+
target_jira_status = IMPACT_TO_JIRA_STATUS_MAP.get(instance.status)
|
|
350
|
+
if target_jira_status is None:
|
|
351
|
+
logger.debug(
|
|
352
|
+
"Skipping post_save status sync: no Jira mapping for status %s (incident #%s)",
|
|
353
|
+
instance.status,
|
|
354
|
+
getattr(instance, "id", "unknown"),
|
|
355
|
+
)
|
|
356
|
+
return
|
|
357
|
+
try:
|
|
358
|
+
_set_impact_to_jira_cache(instance.id, "status", target_jira_status)
|
|
359
|
+
client.transition_issue_auto(
|
|
360
|
+
instance.jira_ticket.id, target_jira_status, RAID_JIRA_WORKFLOW_NAME
|
|
361
|
+
)
|
|
362
|
+
logger.info(
|
|
363
|
+
"Post-save synced status %s to Jira ticket %s for incident #%s",
|
|
364
|
+
instance.status,
|
|
365
|
+
instance.jira_ticket.id,
|
|
366
|
+
getattr(instance, "id", "unknown"),
|
|
367
|
+
)
|
|
368
|
+
except Exception:
|
|
369
|
+
logger.exception(
|
|
370
|
+
"Failed post-save status sync %s to Jira ticket %s for incident %s",
|
|
371
|
+
instance.status,
|
|
372
|
+
instance.jira_ticket.id,
|
|
373
|
+
getattr(instance, "id", "unknown"),
|
|
374
|
+
)
|
firefighter/raid/utils.py
CHANGED
|
@@ -2,9 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
from functools import cache
|
|
5
|
-
from typing import
|
|
6
|
-
|
|
7
|
-
T = TypeVar("T")
|
|
5
|
+
from typing import Any
|
|
8
6
|
|
|
9
7
|
logger = logging.getLogger(__name__)
|
|
10
8
|
|
|
@@ -40,3 +38,15 @@ def get_domain_from_email(email: str) -> str:
|
|
|
40
38
|
domain_parts = domain.split(".")
|
|
41
39
|
|
|
42
40
|
return (".".join(domain_parts[-2:]) if len(domain_parts) > 2 else domain).lower()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def normalize_cache_value(value: Any) -> str:
|
|
44
|
+
"""Normalize cache values for loop-prevention keys."""
|
|
45
|
+
if value is None:
|
|
46
|
+
return ""
|
|
47
|
+
if isinstance(value, str):
|
|
48
|
+
return value.strip().lower()
|
|
49
|
+
try:
|
|
50
|
+
return str(int(value))
|
|
51
|
+
except (TypeError, ValueError):
|
|
52
|
+
return str(value).strip().lower()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: firefighter-incident
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.37
|
|
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=Brj7IANCvq7zHGT7mm_VDO1_vV7OFwt6Zpt4gUwP4pM,1532
|
|
7
7
|
manage.py,sha256=5ivHGD13C6nJ8QvltKsJ9T9akA5he8da70HLWaEP3k8,689
|
|
8
8
|
firefighter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
firefighter/_version.py,sha256=
|
|
9
|
+
firefighter/_version.py,sha256=BV1Ma7idHU0hCvzfaqU-vGA4N-4x9mQtKafasKpzadA,706
|
|
10
10
|
firefighter/api/__init__.py,sha256=JQW0Bv6xwGqy7ioxx3h6UGMzkkJ4DntDpbvV1Ncgi8k,136
|
|
11
11
|
firefighter/api/admin.py,sha256=Q6f37xwf-i0xypFx6zU7r6bYxsSvLm66naZSHUK13JM,4621
|
|
12
12
|
firefighter/api/apps.py,sha256=P5uU1_gMrDfzurdMbfqw1Bnb2uNKKcMq17WBPg2sLhc,204
|
|
@@ -186,13 +186,13 @@ firefighter/incidents/models/__init__.py,sha256=FLVyBwIdyxLdgSvXRAKC3fry9Ywwqlqh
|
|
|
186
186
|
firefighter/incidents/models/environment.py,sha256=51txwua3dCrWZ1iSG3ZA8rbDn9c00pyMAZujl9gwE5c,827
|
|
187
187
|
firefighter/incidents/models/group.py,sha256=VrVL315VFUvKW69AZuRUBg1h0jZJvn8zWeMxMOWec1Y,700
|
|
188
188
|
firefighter/incidents/models/impact.py,sha256=D9NngMtg4XdDWnMgdVYaWCoUZ-fMXTvfL0eTEk9sc7M,4854
|
|
189
|
-
firefighter/incidents/models/incident.py,sha256=
|
|
189
|
+
firefighter/incidents/models/incident.py,sha256=qyg_1NB4cppkcPpwHeswynVVc2Dx2Ttd3fIUkXI9HTg,30337
|
|
190
190
|
firefighter/incidents/models/incident_category.py,sha256=FSUkI9pOYddzVscGE0bpz_axWgBXDE8-5sf1oDZ_784,7714
|
|
191
191
|
firefighter/incidents/models/incident_cost.py,sha256=juwOfJKRaNQpOHkRUCHShDDba0FU98YjRPkU4I0ofAU,1346
|
|
192
192
|
firefighter/incidents/models/incident_cost_type.py,sha256=wm8diry_VySJzIjC9M3Yavv2tYbvJgpN9UDb2gFRuH4,845
|
|
193
193
|
firefighter/incidents/models/incident_membership.py,sha256=vvvBvYPxNlM98KdF81cMrDif8_Wl5TqqNkmf_z9lZO8,1745
|
|
194
194
|
firefighter/incidents/models/incident_role_type.py,sha256=YxtQmsmZQRVxa_pSVe-lIVEoZN5k56cR7o8soWCcFng,2104
|
|
195
|
-
firefighter/incidents/models/incident_update.py,sha256=
|
|
195
|
+
firefighter/incidents/models/incident_update.py,sha256=TO8moYMHRGbmZmzs3yWiN1kwfF3i7vDOx7lXmW6iidc,4437
|
|
196
196
|
firefighter/incidents/models/metric_type.py,sha256=nmc7LANTMjwQmm6LCfLhv4aF7ClEBV2L4PAu1wGfV3w,2981
|
|
197
197
|
firefighter/incidents/models/milestone_type.py,sha256=SToXfQncNkGWLs4xZDNtT0lM4lj-9Pj9mZvr4JI85Fs,1827
|
|
198
198
|
firefighter/incidents/models/priority.py,sha256=5bWsuurbE6TFtMczgCBX6uYkb-nBdTC2ABV-d7b9iw0,2199
|
|
@@ -200,7 +200,7 @@ firefighter/incidents/models/severity.py,sha256=a9p0l8oexr1Ve6pBIz-1rpUzDJAZ-PxH
|
|
|
200
200
|
firefighter/incidents/models/user.py,sha256=Aok7g3d0uB7_gq8aXSYjEt7ogPpCkGX9JxNyjjE8XsU,3193
|
|
201
201
|
firefighter/incidents/static/css/incident.css,sha256=48f03RWvchTz2Te2xSBTRd28qOlLO212sxbGWBd7d2M,2811
|
|
202
202
|
firefighter/incidents/static/css/main.css,sha256=ZYaP3CbVQ1PMjmaWq91SQAbUBhW1BciuzJ52dvJI0fM,48
|
|
203
|
-
firefighter/incidents/static/css/main.min.css,sha256=
|
|
203
|
+
firefighter/incidents/static/css/main.min.css,sha256=6uPLzrqwP-Gkl_XjHt4xRuDIUQ_vTJ5GHmOAVVdPeUQ,120279
|
|
204
204
|
firefighter/incidents/static/css/tailwind.css,sha256=afzjUc4YqJ6TYcYEWJbEMyx6fZ4m9b1ORqMpGTdfps8,977
|
|
205
205
|
firefighter/incidents/static/img/gameday.png,sha256=CaoUFsi5GsdcVOfLFOx-8zpux-gwbj4a4V8bmfWk-x0,6391
|
|
206
206
|
firefighter/incidents/static/img/logo-firefighter.png,sha256=eN7FJa9I1rj6-ohk4nEah8v0aBfHheeShjFWGMciRz0,2090
|
|
@@ -251,7 +251,7 @@ firefighter/incidents/templates/pages/docs_metrics.html,sha256=q10CCPwjujuj9_h7M
|
|
|
251
251
|
firefighter/incidents/templates/pages/incident_category_detail.html,sha256=65vhQ3l3cWlLUYilfKHPuMn7lEfRJFjokiOeYUpeTTY,5425
|
|
252
252
|
firefighter/incidents/templates/pages/incident_category_list.html,sha256=gXLPk9N7R585xqmA6JpL06J7hsxAXQvnsShesK2HR9o,1731
|
|
253
253
|
firefighter/incidents/templates/pages/incident_create.html,sha256=syDS8EqmdcCf6z2dJox5gSPTOsyNdq0jFiDdzVc_vmU,2280
|
|
254
|
-
firefighter/incidents/templates/pages/incident_detail.html,sha256=
|
|
254
|
+
firefighter/incidents/templates/pages/incident_detail.html,sha256=7KbrQMymcPUw0qbF4m08V9afsxrWxml6iGzdk-q5qPk,16238
|
|
255
255
|
firefighter/incidents/templates/pages/incident_list.html,sha256=IWTZyuIeG1RVCZGNLQ_N3Jg7a_Ush6YbQXWUy0xVuV8,2339
|
|
256
256
|
firefighter/incidents/templates/pages/incident_role_types_detail.html,sha256=LVoFbW9rrP4Yln_Ld-Jwe1JvhhvgwpxZgpdNzbrLWsA,4099
|
|
257
257
|
firefighter/incidents/templates/pages/incident_role_types_list.html,sha256=SRBhDIfne-bX4C-EhYqyCktIksR4Rd5GPMkYfE6El1o,1556
|
|
@@ -264,7 +264,7 @@ firefighter/incidents/views/date_filter.py,sha256=fUhTjkBulMokI5tAHuqNDVv1dyspjm
|
|
|
264
264
|
firefighter/incidents/views/date_utils.py,sha256=Q2i-84hXm1vuXy7-1x1SSteWXzXUzh4b0a79nmTEfWA,5733
|
|
265
265
|
firefighter/incidents/views/errors.py,sha256=yDuH0YOdGf-voVNEC51yR9Ie3OU-az7g2EqWs_uV1Kk,7855
|
|
266
266
|
firefighter/incidents/views/reports.py,sha256=1Iegx04w-oHw4cj7u9w2_s7T_e9FH5I6RRPTwDZwZhg,20973
|
|
267
|
-
firefighter/incidents/views/views.py,sha256=
|
|
267
|
+
firefighter/incidents/views/views.py,sha256=ynMYwEi4vKHpYwrH6kEAiWKY91NVF8QSZfKy5thQuXg,12287
|
|
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
|
|
@@ -328,22 +328,22 @@ firefighter/pagerduty/views/oncall_trigger.py,sha256=LYHpWyEaR6O8NazmsTl5ydtw1XH
|
|
|
328
328
|
firefighter/raid/__init__.py,sha256=nMNmvHCSkyLQsdhTow7myMU62vXk1e755gUntVfFFlY,154
|
|
329
329
|
firefighter/raid/admin.py,sha256=WhIHaRAv7JPp2NH27w7_0JfvGHrvoyRJhYr3_WwedrA,1117
|
|
330
330
|
firefighter/raid/apps.py,sha256=olDKua1rqhhIJUhCu6A2PnPWloW_jbeD4XWL94b2owo,1117
|
|
331
|
-
firefighter/raid/client.py,sha256=
|
|
331
|
+
firefighter/raid/client.py,sha256=03YJFZYkJe7PsgjLXAISOwETKd4KBkMJjyOOuAv5I08,8336
|
|
332
332
|
firefighter/raid/forms.py,sha256=XivNADFHOl2ewNRXev17HthDIUvwt4pdH9t4So-BE-A,11968
|
|
333
333
|
firefighter/raid/messages.py,sha256=e75kwi0hCe5ChwU4t-_6Q3Rcy22MLLdVSsYyjvG2SCM,5542
|
|
334
334
|
firefighter/raid/models.py,sha256=29Smci739K1ZdcMu7uXYvoVEhgDpwLQoCzBbc5wvwhs,2211
|
|
335
335
|
firefighter/raid/resources.py,sha256=39GhITs3OAWA1eSPZme-rLd818kuz7gwYzdN38zNz8Y,436
|
|
336
|
-
firefighter/raid/serializers.py,sha256=
|
|
336
|
+
firefighter/raid/serializers.py,sha256=MGswzLV1o4Ke19szyfevWzuWeVVNW6LPopXbwhc4nHg,22327
|
|
337
337
|
firefighter/raid/service.py,sha256=tkMttdao_4s7In66jQhN1CN0UFzR2OeXYSPQDXfLz0I,8577
|
|
338
338
|
firefighter/raid/types.py,sha256=E0ZUjBCMwCgr0eSX3CfRTpmq0n-jMans1WxoYSnj7xg,477
|
|
339
339
|
firefighter/raid/urls.py,sha256=oESkDY2tfZcnPGUgULqixvbV3Z7YsZfeI10RX3A5tZY,924
|
|
340
|
-
firefighter/raid/utils.py,sha256=
|
|
340
|
+
firefighter/raid/utils.py,sha256=QRNhjZSfYzxjVLHgpoH3rneYaH53IBoE0cCEGYrGC6U,1422
|
|
341
341
|
firefighter/raid/migrations/0001_initial_oss.py,sha256=oZQ44dHboLeHbvVyziHX_hhdis8H_Buv2W3jHiDzQhA,6259
|
|
342
342
|
firefighter/raid/migrations/0002_featureteam_remove_qualifierrotation_jira_user_and_more.py,sha256=1UkwuNqqjtRJ_xlgNMs9qn2ryLcssQN06ZBCL296CWw,1034
|
|
343
343
|
firefighter/raid/migrations/0003_delete_raidarea.py,sha256=M_XkKCu73ib2H09co2L-ssLQakJJFNOfqJpKrzOYP2Y,332
|
|
344
344
|
firefighter/raid/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
345
345
|
firefighter/raid/signals/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
346
|
-
firefighter/raid/signals/incident_updated.py,sha256=
|
|
346
|
+
firefighter/raid/signals/incident_updated.py,sha256=2rT7KLCyOZwd9HovmYs9_h61aKL-6gwKqBP-144-BsA,13957
|
|
347
347
|
firefighter/raid/tasks/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
|
|
348
348
|
firefighter/raid/views/__init__.py,sha256=C3WhAJfEoUasi2afHPuLpKiuRYixK-tc3j0-2Rw_g3E,5210
|
|
349
349
|
firefighter/slack/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -482,6 +482,7 @@ firefighter_tests/test_incidents/test_models/test_incident_category.py,sha256=aR
|
|
|
482
482
|
firefighter_tests/test_incidents/test_models/test_incident_model.py,sha256=AWyWfQYcHNP9GPizIo0wRxNGTJTEJnAwNSd4UmRq-dk,8626
|
|
483
483
|
firefighter_tests/test_incidents/test_models/test_migrations/test_incident_migrations.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
484
484
|
firefighter_tests/test_incidents/test_utils/test_date_utils.py,sha256=ogP7qOEwItL4YGI5gbQPVssOS9ilwiuZC8OrT2qngBY,6568
|
|
485
|
+
firefighter_tests/test_incidents/test_views/test_incident_detail_jira_postmortem.py,sha256=WM1nr3XLGMqOEyr4e6XsWcU01aH4spiGnXd9YsVoVZY,1925
|
|
485
486
|
firefighter_tests/test_incidents/test_views/test_incident_detail_view.py,sha256=lkCIRfz99Ea0o0Id08LFWrjXLDmHv6XvezaSsjg-eYQ,871
|
|
486
487
|
firefighter_tests/test_incidents/test_views/test_index_view.py,sha256=InpxbaWOFwRn4YWeIKZhj17vMymrQQf2p2LFhe2Bcdw,816
|
|
487
488
|
firefighter_tests/test_jira_app/__init__.py,sha256=JxZ3v-0kiHOoO-N3kR8NHTmD8tEvuEYKW1GX_S1ZLMY,33
|
|
@@ -493,14 +494,15 @@ firefighter_tests/test_jira_app/test_postmortem_issue_link.py,sha256=CF0UDmHOY08
|
|
|
493
494
|
firefighter_tests/test_jira_app/test_postmortem_service.py,sha256=gUimkgf3NNIxHFDeZ5GNNzLHdaPKQCZDrTPdHmbyqDc,15120
|
|
494
495
|
firefighter_tests/test_jira_app/test_timeline_template.py,sha256=_PtFnIib2HfjyylNRQXcjvdhrsoAJICOKauIDsYFQRk,4902
|
|
495
496
|
firefighter_tests/test_raid/conftest.py,sha256=i_TOquYIMLDyVQ97uqxTqPJszVz4qq7L_Q7YJxTuS1o,4090
|
|
497
|
+
firefighter_tests/test_raid/test_jira_status_sync.py,sha256=h0RTMAxL9paWXgF5zzlglqUNLfblmi-9dhrTZdh1ayk,7482
|
|
496
498
|
firefighter_tests/test_raid/test_raid_alert_p4_p5.py,sha256=rz9orbt1E1vJ5POQyVZ6-SEPvqB55-xhwIWHicdfgDg,9356
|
|
497
499
|
firefighter_tests/test_raid/test_raid_client.py,sha256=KTqELERpWno7XhF9LpabpxkHoJiWWrryUg5LHi5Yfjo,22456
|
|
498
500
|
firefighter_tests/test_raid/test_raid_client_users.py,sha256=9uma1wBhaiCoG75XAZHqpT8oGTnqFJRMCi7a3XctNtM,3631
|
|
499
501
|
firefighter_tests/test_raid/test_raid_forms.py,sha256=8hiXftYPO_lY0heKHqoreUW2s8AcedUme48wTq4hwNE,21931
|
|
500
502
|
firefighter_tests/test_raid/test_raid_models.py,sha256=nq-fVClB_P24W8WrZruOPt8wlHUVGYI7wxJR7tH6AnM,5042
|
|
501
|
-
firefighter_tests/test_raid/test_raid_serializers.py,sha256=
|
|
503
|
+
firefighter_tests/test_raid/test_raid_serializers.py,sha256=6gjt8n8BYZwg6bXyuIieBQ9L13N4T_WFi3A0MES-Rpk,23186
|
|
502
504
|
firefighter_tests/test_raid/test_raid_service.py,sha256=AqVyrRjW2tr0sfbXS4lGlJ7mcxB2ACEXAR8Bv0pXnj0,16755
|
|
503
|
-
firefighter_tests/test_raid/test_raid_signals.py,sha256=
|
|
505
|
+
firefighter_tests/test_raid/test_raid_signals.py,sha256=VrCBF3e-mEMkTVbqjNb7QJaQ5eQVVERJXBlCzFQTvFM,9210
|
|
504
506
|
firefighter_tests/test_raid/test_raid_transitions.py,sha256=mtmMKwukxmZSM-R619BQ3Z_2AB-qY6imvDgUF0A3_tw,4784
|
|
505
507
|
firefighter_tests/test_raid/test_raid_utils.py,sha256=i6JBwim1G-qynwxprNZekxl9K7Vis4FFvNkw3wT2jTM,1016
|
|
506
508
|
firefighter_tests/test_raid/test_raid_views.py,sha256=paAhh4k2EDlmG1ehwNhMuYIhr1okqrvM7xlkaTAo2V0,6825
|
|
@@ -526,10 +528,10 @@ firefighter_tests/test_slack/views/modals/test_opening_unified.py,sha256=OejtLyc
|
|
|
526
528
|
firefighter_tests/test_slack/views/modals/test_postmortem_modal.py,sha256=zNN40sIRSM5w_kyOcQ-AODkH5WpVxkSGVXkh9rMgmQ0,2378
|
|
527
529
|
firefighter_tests/test_slack/views/modals/test_send_sos.py,sha256=_rE6jD-gOzcGyhlY0R9GzlGtPx65oOOguJYdENgxtLc,1289
|
|
528
530
|
firefighter_tests/test_slack/views/modals/test_status.py,sha256=oQzPfwdg2tkbo9nfkO1GfS3WydxqSC6vy1AZjZDKT30,2226
|
|
529
|
-
firefighter_tests/test_slack/views/modals/test_update_status.py,sha256=
|
|
531
|
+
firefighter_tests/test_slack/views/modals/test_update_status.py,sha256=y33RKdjiPsN2hcKQB6tMbNwSMy8KMiZKjf4VdFb5_vc,56581
|
|
530
532
|
firefighter_tests/test_slack/views/modals/test_utils.py,sha256=DJd2n9q6fFu8UuCRdiq9U_Cn19MdnC5c-ydLLrk6rkc,5218
|
|
531
|
-
firefighter_incident-0.0.
|
|
532
|
-
firefighter_incident-0.0.
|
|
533
|
-
firefighter_incident-0.0.
|
|
534
|
-
firefighter_incident-0.0.
|
|
535
|
-
firefighter_incident-0.0.
|
|
533
|
+
firefighter_incident-0.0.37.dist-info/METADATA,sha256=JGhAeFgwEjVwqN25L93YHvwHWg62s-FZbo8haHkuhh4,5570
|
|
534
|
+
firefighter_incident-0.0.37.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
535
|
+
firefighter_incident-0.0.37.dist-info/entry_points.txt,sha256=c13meJbv7YNmYz7MipMOQwzQ5IeFOPXUBYAJ44XMQsM,61
|
|
536
|
+
firefighter_incident-0.0.37.dist-info/licenses/LICENSE,sha256=krRiGp-a9-1nH1bWpBEdxyTKLhjLmn6DMVVoIb0zF90,1087
|
|
537
|
+
firefighter_incident-0.0.37.dist-info/RECORD,,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Test incident detail view displays Jira post-mortem links."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from django.apps import apps
|
|
7
|
+
from django.test import Client
|
|
8
|
+
from django.urls import reverse
|
|
9
|
+
|
|
10
|
+
from firefighter.incidents.models import Incident
|
|
11
|
+
from firefighter.incidents.models.user import User
|
|
12
|
+
|
|
13
|
+
pytestmark = pytest.mark.skipif(
|
|
14
|
+
not apps.is_installed("firefighter.jira_app"),
|
|
15
|
+
reason="Jira app not installed",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.mark.django_db
|
|
20
|
+
def test_incident_detail_view_with_jira_postmortem(
|
|
21
|
+
client: Client, incident_saved: Incident, admin_user: User
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Test that Jira post-mortem link is displayed when it exists."""
|
|
24
|
+
from firefighter.jira_app.models import JiraPostMortem
|
|
25
|
+
|
|
26
|
+
# Create a Jira post-mortem for the incident
|
|
27
|
+
JiraPostMortem.objects.create(
|
|
28
|
+
incident=incident_saved,
|
|
29
|
+
jira_issue_key="INCIDENT-123",
|
|
30
|
+
jira_issue_id="10001",
|
|
31
|
+
created_by=admin_user,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
client.force_login(admin_user)
|
|
35
|
+
response = client.get(
|
|
36
|
+
reverse("incidents:incident-detail", args=[incident_saved.id])
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
assert response.status_code == 200
|
|
40
|
+
response_content = str(response.content)
|
|
41
|
+
|
|
42
|
+
# Check that the post-mortem section is present
|
|
43
|
+
assert "Post-mortem" in response_content
|
|
44
|
+
|
|
45
|
+
# Check that the Jira issue key is displayed
|
|
46
|
+
assert "INCIDENT-123" in response_content
|
|
47
|
+
|
|
48
|
+
# Check that the URL is correctly formed
|
|
49
|
+
assert "browse/INCIDENT-123" in response_content
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.mark.django_db
|
|
53
|
+
def test_incident_detail_view_without_jira_postmortem(
|
|
54
|
+
client: Client, incident_saved: Incident, admin_user: User
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Test that incident detail view works without Jira post-mortem."""
|
|
57
|
+
client.force_login(admin_user)
|
|
58
|
+
response = client.get(
|
|
59
|
+
reverse("incidents:incident-detail", args=[incident_saved.id])
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
assert response.status_code == 200
|
|
63
|
+
# Page should load successfully even without post-mortem
|