firefighter-incident 0.0.36__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.
@@ -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
- f"Trying to close Jira ticket for incident {incident.id} but no Jira ticket found"
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
- # Determine if we should close the ticket based on status and priority
44
- should_close = False
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
- if incident_update.status == IncidentStatus.CLOSED:
47
- # Always close on CLOSED regardless of priority
48
- should_close = True
49
- elif incident_update.status == IncidentStatus.MITIGATED:
50
- # Only close on MITIGATED if incident doesn't need postmortem (P3+)
51
- should_close = not incident.needs_postmortem
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
- if should_close:
56
- status_label = incident_update.status.label if incident_update.status else "Unknown"
57
- logger.info(f"Closing Jira ticket for incident {incident.id} (status: {status_label})")
58
- client.close_issue(issue_id=incident.jira_ticket.id)
59
- # XXX We may want to add a comment if there is an incident update message on close
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 TypeVar
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.36
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=0ktbVh2xRI7xnrojV_sDPVhrN753EK8vWN68JQalwWk,706
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=YM-mQXoNcvO5BBdUjDqfvb6zPQmFO281qDa8PIQJ6Y4,29621
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=lCslA0Bz7qI9qORr0-Wir6yWzp0AeuCKsYHhLyKKukQ,4612
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=P9bd2u-TRtZClyutuYw7R5JT9YZtS-ogWiWhZVVAl0M,119104
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
@@ -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=txHIRPxvJkcg40XJGseyj3KOOVep8WMdrlDhJ5oVP_Q,8085
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=4U6UFUNVR68lBYDzjYp8j1JzmfSftASOp1nQifCRXYQ,11941
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=OhsuFb46lokVxjyfeBFLMcMIGzjkTrrdd6DCXUJym0E,1111
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=Al1S1BOWAd6s97ITG0Yg181hOOLR5Byx8OO9hM5kP9E,2231
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
@@ -494,14 +494,15 @@ firefighter_tests/test_jira_app/test_postmortem_issue_link.py,sha256=CF0UDmHOY08
494
494
  firefighter_tests/test_jira_app/test_postmortem_service.py,sha256=gUimkgf3NNIxHFDeZ5GNNzLHdaPKQCZDrTPdHmbyqDc,15120
495
495
  firefighter_tests/test_jira_app/test_timeline_template.py,sha256=_PtFnIib2HfjyylNRQXcjvdhrsoAJICOKauIDsYFQRk,4902
496
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
497
498
  firefighter_tests/test_raid/test_raid_alert_p4_p5.py,sha256=rz9orbt1E1vJ5POQyVZ6-SEPvqB55-xhwIWHicdfgDg,9356
498
499
  firefighter_tests/test_raid/test_raid_client.py,sha256=KTqELERpWno7XhF9LpabpxkHoJiWWrryUg5LHi5Yfjo,22456
499
500
  firefighter_tests/test_raid/test_raid_client_users.py,sha256=9uma1wBhaiCoG75XAZHqpT8oGTnqFJRMCi7a3XctNtM,3631
500
501
  firefighter_tests/test_raid/test_raid_forms.py,sha256=8hiXftYPO_lY0heKHqoreUW2s8AcedUme48wTq4hwNE,21931
501
502
  firefighter_tests/test_raid/test_raid_models.py,sha256=nq-fVClB_P24W8WrZruOPt8wlHUVGYI7wxJR7tH6AnM,5042
502
- firefighter_tests/test_raid/test_raid_serializers.py,sha256=xkMK9npvDMAr2pUpP4mFbLszrR6qyTzsJRd-uNOzEhM,22560
503
+ firefighter_tests/test_raid/test_raid_serializers.py,sha256=6gjt8n8BYZwg6bXyuIieBQ9L13N4T_WFi3A0MES-Rpk,23186
503
504
  firefighter_tests/test_raid/test_raid_service.py,sha256=AqVyrRjW2tr0sfbXS4lGlJ7mcxB2ACEXAR8Bv0pXnj0,16755
504
- firefighter_tests/test_raid/test_raid_signals.py,sha256=QkGd3alp5O6t5kz66ukpNY-uZdIKJsqxvEPc8exORP4,8974
505
+ firefighter_tests/test_raid/test_raid_signals.py,sha256=VrCBF3e-mEMkTVbqjNb7QJaQ5eQVVERJXBlCzFQTvFM,9210
505
506
  firefighter_tests/test_raid/test_raid_transitions.py,sha256=mtmMKwukxmZSM-R619BQ3Z_2AB-qY6imvDgUF0A3_tw,4784
506
507
  firefighter_tests/test_raid/test_raid_utils.py,sha256=i6JBwim1G-qynwxprNZekxl9K7Vis4FFvNkw3wT2jTM,1016
507
508
  firefighter_tests/test_raid/test_raid_views.py,sha256=paAhh4k2EDlmG1ehwNhMuYIhr1okqrvM7xlkaTAo2V0,6825
@@ -527,10 +528,10 @@ firefighter_tests/test_slack/views/modals/test_opening_unified.py,sha256=OejtLyc
527
528
  firefighter_tests/test_slack/views/modals/test_postmortem_modal.py,sha256=zNN40sIRSM5w_kyOcQ-AODkH5WpVxkSGVXkh9rMgmQ0,2378
528
529
  firefighter_tests/test_slack/views/modals/test_send_sos.py,sha256=_rE6jD-gOzcGyhlY0R9GzlGtPx65oOOguJYdENgxtLc,1289
529
530
  firefighter_tests/test_slack/views/modals/test_status.py,sha256=oQzPfwdg2tkbo9nfkO1GfS3WydxqSC6vy1AZjZDKT30,2226
530
- firefighter_tests/test_slack/views/modals/test_update_status.py,sha256=vbHGx6dkM_0swE1vJ0HrkhI1oJzD_WHZuIQ-_arAxXo,55686
531
+ firefighter_tests/test_slack/views/modals/test_update_status.py,sha256=y33RKdjiPsN2hcKQB6tMbNwSMy8KMiZKjf4VdFb5_vc,56581
531
532
  firefighter_tests/test_slack/views/modals/test_utils.py,sha256=DJd2n9q6fFu8UuCRdiq9U_Cn19MdnC5c-ydLLrk6rkc,5218
532
- firefighter_incident-0.0.36.dist-info/METADATA,sha256=lTBD-i5dhhYhEu9Jy1GK31W1V_HbnE1bHiNP0fItrjM,5570
533
- firefighter_incident-0.0.36.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
534
- firefighter_incident-0.0.36.dist-info/entry_points.txt,sha256=c13meJbv7YNmYz7MipMOQwzQ5IeFOPXUBYAJ44XMQsM,61
535
- firefighter_incident-0.0.36.dist-info/licenses/LICENSE,sha256=krRiGp-a9-1nH1bWpBEdxyTKLhjLmn6DMVVoIb0zF90,1087
536
- firefighter_incident-0.0.36.dist-info/RECORD,,
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,,