firefighter-incident 0.0.14__py3-none-any.whl → 0.0.16__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 (64) hide show
  1. firefighter/_version.py +2 -2
  2. firefighter/api/serializers.py +9 -0
  3. firefighter/confluence/signals/incident_updated.py +2 -2
  4. firefighter/incidents/enums.py +22 -2
  5. firefighter/incidents/forms/closure_reason.py +45 -0
  6. firefighter/incidents/forms/unified_incident.py +406 -0
  7. firefighter/incidents/forms/update_status.py +87 -1
  8. firefighter/incidents/migrations/0027_add_closure_fields.py +40 -0
  9. firefighter/incidents/migrations/0028_add_closure_reason_constraint.py +33 -0
  10. firefighter/incidents/migrations/0029_add_custom_fields_to_incident.py +22 -0
  11. firefighter/incidents/models/incident.py +32 -5
  12. firefighter/incidents/static/css/main.min.css +1 -1
  13. firefighter/incidents/templates/layouts/partials/status_pill.html +1 -1
  14. firefighter/incidents/views/reports.py +3 -3
  15. firefighter/raid/apps.py +9 -26
  16. firefighter/raid/client.py +2 -2
  17. firefighter/raid/forms.py +75 -238
  18. firefighter/raid/signals/incident_created.py +38 -13
  19. firefighter/raid/signals/incident_updated.py +3 -2
  20. firefighter/slack/messages/slack_messages.py +19 -4
  21. firefighter/slack/rules.py +1 -1
  22. firefighter/slack/signals/create_incident_conversation.py +6 -0
  23. firefighter/slack/signals/incident_updated.py +7 -1
  24. firefighter/slack/views/modals/__init__.py +4 -0
  25. firefighter/slack/views/modals/base_modal/form_utils.py +63 -0
  26. firefighter/slack/views/modals/close.py +15 -2
  27. firefighter/slack/views/modals/closure_reason.py +193 -0
  28. firefighter/slack/views/modals/open.py +60 -13
  29. firefighter/slack/views/modals/opening/details/unified.py +203 -0
  30. firefighter/slack/views/modals/opening/select_impact.py +1 -1
  31. firefighter/slack/views/modals/opening/set_details.py +3 -2
  32. firefighter/slack/views/modals/postmortem.py +10 -2
  33. firefighter/slack/views/modals/update_status.py +28 -2
  34. firefighter/slack/views/modals/utils.py +51 -0
  35. {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/METADATA +1 -1
  36. {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/RECORD +62 -38
  37. firefighter_tests/test_incidents/test_enums.py +100 -0
  38. firefighter_tests/test_incidents/test_forms/conftest.py +179 -0
  39. firefighter_tests/test_incidents/test_forms/test_closure_reason.py +91 -0
  40. firefighter_tests/test_incidents/test_forms/test_unified_incident_form.py +570 -0
  41. firefighter_tests/test_incidents/test_forms/test_unified_incident_form_integration.py +581 -0
  42. firefighter_tests/test_incidents/test_forms/test_unified_incident_form_p4_p5.py +410 -0
  43. firefighter_tests/test_incidents/test_forms/test_update_status_workflow.py +343 -0
  44. firefighter_tests/test_incidents/test_forms/test_workflow_transitions.py +167 -0
  45. firefighter_tests/test_incidents/test_models/test_incident_model.py +68 -0
  46. firefighter_tests/test_raid/conftest.py +154 -0
  47. firefighter_tests/test_raid/test_p1_p3_jira_fields.py +372 -0
  48. firefighter_tests/test_raid/test_raid_forms.py +10 -253
  49. firefighter_tests/test_raid/test_raid_signals.py +187 -0
  50. firefighter_tests/test_slack/messages/__init__.py +0 -0
  51. firefighter_tests/test_slack/messages/test_slack_messages.py +367 -0
  52. firefighter_tests/test_slack/views/modals/conftest.py +140 -0
  53. firefighter_tests/test_slack/views/modals/test_close.py +65 -3
  54. firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +138 -0
  55. firefighter_tests/test_slack/views/modals/test_form_utils_multiple_choice.py +249 -0
  56. firefighter_tests/test_slack/views/modals/test_open.py +146 -2
  57. firefighter_tests/test_slack/views/modals/test_opening_unified.py +421 -0
  58. firefighter_tests/test_slack/views/modals/test_update_status.py +327 -3
  59. firefighter_tests/test_slack/views/modals/test_utils.py +135 -0
  60. firefighter/raid/views/open_normal.py +0 -139
  61. firefighter/slack/views/modals/opening/details/critical.py +0 -88
  62. {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/WHEEL +0 -0
  63. {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/entry_points.txt +0 -0
  64. {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,421 @@
1
+ """Tests for unified incident opening modal."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+
6
+ import pytest
7
+
8
+ from firefighter.incidents.models.impact import LevelChoices
9
+ from firefighter.slack.views.modals.base_modal.form_utils import SlackForm
10
+ from firefighter.slack.views.modals.opening.details.unified import (
11
+ OpeningUnifiedModal,
12
+ UnifiedIncidentFormSlack,
13
+ )
14
+
15
+
16
+ @pytest.mark.django_db
17
+ class TestUnifiedIncidentFormSlack:
18
+ """Test Slack-specific version of unified form."""
19
+
20
+ def test_form_initializes_with_impacts_and_response_type(
21
+ self, priority_factory, impact_level_factory
22
+ ):
23
+ """Form should properly receive impacts_data and response_type."""
24
+ priority_factory(value=1, default=True)
25
+ customer_impact = impact_level_factory(value=LevelChoices.HIGH, impact__name="Customers")
26
+
27
+ impacts_data = {"customers_impact": customer_impact}
28
+
29
+ form = UnifiedIncidentFormSlack(
30
+ impacts_data=impacts_data,
31
+ response_type="critical",
32
+ )
33
+
34
+ # Should configure field visibility
35
+ assert "zendesk_ticket_id" in form.fields
36
+ assert "suggested_team_routing" not in form.fields
37
+
38
+ def test_slack_blocks_generation_with_multiple_choice_fields(
39
+ self, priority_factory, environment_factory
40
+ ):
41
+ """Slack blocks should generate correctly with multiple choice fields."""
42
+ priority_factory(value=1, default=True)
43
+ environment_factory(value="PRD", default=True)
44
+ environment_factory(value="STG", default=False)
45
+
46
+ # Use SlackForm wrapper to generate blocks
47
+ slack_form = SlackForm(UnifiedIncidentFormSlack)(
48
+ impacts_data={},
49
+ response_type="critical",
50
+ )
51
+
52
+ # Should generate blocks without errors
53
+ blocks = slack_form.slack_blocks()
54
+ assert len(blocks) > 0
55
+
56
+ # Find environment block (multi-select)
57
+ env_blocks = [b for b in blocks if hasattr(b, "block_id") and b.block_id == "environment"]
58
+ assert len(env_blocks) == 1
59
+
60
+ env_block = env_blocks[0]
61
+ assert env_block.element.type == "multi_static_select"
62
+
63
+ def test_priority_field_is_hidden(self, priority_factory):
64
+ """Priority field should not appear in Slack blocks."""
65
+ priority_factory(value=1, default=True)
66
+
67
+ # Use SlackForm wrapper to generate blocks
68
+ slack_form = SlackForm(UnifiedIncidentFormSlack)(
69
+ impacts_data={},
70
+ response_type="critical",
71
+ )
72
+
73
+ blocks = slack_form.slack_blocks()
74
+
75
+ # Priority field should not generate a block (it's hidden)
76
+ priority_blocks = [
77
+ b for b in blocks if hasattr(b, "block_id") and b.block_id == "priority"
78
+ ]
79
+ assert len(priority_blocks) == 0
80
+
81
+
82
+ @pytest.mark.django_db
83
+ class TestOpeningUnifiedModal:
84
+ """Test the unified opening modal."""
85
+
86
+ def test_build_modal_fn_passes_context_to_form(
87
+ self, priority_factory, impact_level_factory
88
+ ):
89
+ """build_modal_fn should pass impacts and response_type to form."""
90
+ priority_factory(value=1, default=True)
91
+ customer_impact = impact_level_factory(value=LevelChoices.HIGH, impact__name="Customers")
92
+
93
+ modal = OpeningUnifiedModal()
94
+
95
+ open_incident_context = {
96
+ "response_type": "critical",
97
+ "impact_form_data": {"customers_impact": customer_impact},
98
+ "details_form_data": {},
99
+ }
100
+
101
+ view = modal.build_modal_fn(open_incident_context=open_incident_context)
102
+
103
+ # Should build view without errors
104
+ assert view is not None
105
+ assert view.type == "modal"
106
+ assert len(view.blocks) > 0
107
+
108
+ # Check private_metadata contains our context
109
+ metadata = json.loads(view.private_metadata)
110
+ assert metadata["response_type"] == "critical"
111
+ assert "impact_form_data" in metadata
112
+
113
+ def test_build_modal_fn_critical_incident_hides_feature_team(
114
+ self, priority_factory
115
+ ):
116
+ """Critical incident should not show feature team field."""
117
+ priority_factory(value=1, default=True)
118
+
119
+ modal = OpeningUnifiedModal()
120
+
121
+ open_incident_context = {
122
+ "response_type": "critical",
123
+ "impact_form_data": {},
124
+ "details_form_data": {},
125
+ }
126
+
127
+ view = modal.build_modal_fn(open_incident_context=open_incident_context)
128
+
129
+ # Check that feature team field is not in blocks
130
+ feature_team_blocks = [
131
+ b
132
+ for b in view.blocks
133
+ if hasattr(b, "block_id") and b.block_id == "suggested_team_routing"
134
+ ]
135
+ assert len(feature_team_blocks) == 0
136
+
137
+ def test_build_modal_fn_normal_incident_shows_feature_team(
138
+ self, priority_factory
139
+ ):
140
+ """Normal incident (P4-P5) should show feature team field."""
141
+ priority_factory(value=4, default=True)
142
+
143
+ modal = OpeningUnifiedModal()
144
+
145
+ open_incident_context = {
146
+ "response_type": "normal",
147
+ "impact_form_data": {},
148
+ "details_form_data": {},
149
+ }
150
+
151
+ view = modal.build_modal_fn(open_incident_context=open_incident_context)
152
+
153
+ # Check that feature team field IS in blocks
154
+ feature_team_blocks = [
155
+ b
156
+ for b in view.blocks
157
+ if hasattr(b, "block_id") and b.block_id == "suggested_team_routing"
158
+ ]
159
+ assert len(feature_team_blocks) == 1
160
+
161
+ def test_build_modal_fn_customer_impact_shows_zendesk(
162
+ self, priority_factory, impact_level_factory
163
+ ):
164
+ """Customer impact should show Zendesk field."""
165
+ priority_factory(value=1, default=True)
166
+ customer_impact = impact_level_factory(value=LevelChoices.HIGH, impact__name="Customers")
167
+
168
+ modal = OpeningUnifiedModal()
169
+
170
+ open_incident_context = {
171
+ "response_type": "critical",
172
+ "impact_form_data": {"customers_impact": customer_impact},
173
+ "details_form_data": {},
174
+ }
175
+
176
+ view = modal.build_modal_fn(open_incident_context=open_incident_context)
177
+
178
+ # Check that zendesk field IS in blocks
179
+ zendesk_blocks = [
180
+ b
181
+ for b in view.blocks
182
+ if hasattr(b, "block_id") and b.block_id == "zendesk_ticket_id"
183
+ ]
184
+ assert len(zendesk_blocks) == 1
185
+
186
+ def test_build_modal_fn_seller_impact_shows_seller_fields(
187
+ self, priority_factory, impact_level_factory
188
+ ):
189
+ """Seller impact should show seller-specific fields."""
190
+ priority_factory(value=1, default=True)
191
+ seller_impact = impact_level_factory(value=LevelChoices.LOW, impact__name="Sellers")
192
+
193
+ modal = OpeningUnifiedModal()
194
+
195
+ open_incident_context = {
196
+ "response_type": "critical",
197
+ "impact_form_data": {"sellers_impact": seller_impact},
198
+ "details_form_data": {},
199
+ }
200
+
201
+ view = modal.build_modal_fn(open_incident_context=open_incident_context)
202
+
203
+ # Check seller fields are in blocks
204
+ seller_contract_blocks = [
205
+ b
206
+ for b in view.blocks
207
+ if hasattr(b, "block_id") and b.block_id == "seller_contract_id"
208
+ ]
209
+ assert len(seller_contract_blocks) == 1
210
+
211
+ zoho_blocks = [
212
+ b
213
+ for b in view.blocks
214
+ if hasattr(b, "block_id") and b.block_id == "zoho_desk_ticket_id"
215
+ ]
216
+ assert len(zoho_blocks) == 1
217
+
218
+ def test_build_modal_fn_business_impact_only_hides_customer_seller_fields(
219
+ self, priority_factory, impact_level_factory
220
+ ):
221
+ """Business impact only should not show customer/seller fields."""
222
+ priority_factory(value=1, default=True)
223
+ business_impact = impact_level_factory(value=LevelChoices.HIGH, impact__name="Business")
224
+
225
+ modal = OpeningUnifiedModal()
226
+
227
+ open_incident_context = {
228
+ "response_type": "critical",
229
+ "impact_form_data": {"business_impact": business_impact},
230
+ "details_form_data": {},
231
+ }
232
+
233
+ view = modal.build_modal_fn(open_incident_context=open_incident_context)
234
+
235
+ # Should NOT show customer or seller fields
236
+ zendesk_blocks = [
237
+ b
238
+ for b in view.blocks
239
+ if hasattr(b, "block_id") and b.block_id == "zendesk_ticket_id"
240
+ ]
241
+ assert len(zendesk_blocks) == 0
242
+
243
+ seller_blocks = [
244
+ b
245
+ for b in view.blocks
246
+ if hasattr(b, "block_id") and b.block_id == "seller_contract_id"
247
+ ]
248
+ assert len(seller_blocks) == 0
249
+
250
+ def test_build_modal_fn_employee_impact_only_hides_customer_seller_fields(
251
+ self, priority_factory, impact_level_factory
252
+ ):
253
+ """Employee impact only should not show customer/seller fields."""
254
+ priority_factory(value=1, default=True)
255
+ employee_impact = impact_level_factory(value=LevelChoices.HIGH, impact__name="Employees")
256
+
257
+ modal = OpeningUnifiedModal()
258
+
259
+ open_incident_context = {
260
+ "response_type": "critical",
261
+ "impact_form_data": {"employees_impact": employee_impact},
262
+ "details_form_data": {},
263
+ }
264
+
265
+ view = modal.build_modal_fn(open_incident_context=open_incident_context)
266
+
267
+ # Should NOT show customer or seller fields
268
+ zendesk_blocks = [
269
+ b
270
+ for b in view.blocks
271
+ if hasattr(b, "block_id") and b.block_id == "zendesk_ticket_id"
272
+ ]
273
+ assert len(zendesk_blocks) == 0
274
+
275
+ seller_blocks = [
276
+ b
277
+ for b in view.blocks
278
+ if hasattr(b, "block_id") and b.block_id == "seller_contract_id"
279
+ ]
280
+ assert len(seller_blocks) == 0
281
+
282
+ def test_build_modal_fn_all_impacts_shows_all_fields(
283
+ self, priority_factory, impact_level_factory
284
+ ):
285
+ """All impact types should show all specific fields."""
286
+ priority_factory(value=1, default=True)
287
+ customer_impact = impact_level_factory(value=LevelChoices.LOW, impact__name="Customers")
288
+ seller_impact = impact_level_factory(value=LevelChoices.LOW, impact__name="Sellers")
289
+ employee_impact = impact_level_factory(value=LevelChoices.HIGH, impact__name="Employees")
290
+ business_impact = impact_level_factory(value=LevelChoices.HIGH, impact__name="Business")
291
+
292
+ modal = OpeningUnifiedModal()
293
+
294
+ open_incident_context = {
295
+ "response_type": "critical",
296
+ "impact_form_data": {
297
+ "customers_impact": customer_impact,
298
+ "sellers_impact": seller_impact,
299
+ "employees_impact": employee_impact,
300
+ "business_impact": business_impact,
301
+ },
302
+ "details_form_data": {},
303
+ }
304
+
305
+ view = modal.build_modal_fn(open_incident_context=open_incident_context)
306
+
307
+ # Should show both customer and seller fields
308
+ zendesk_blocks = [
309
+ b
310
+ for b in view.blocks
311
+ if hasattr(b, "block_id") and b.block_id == "zendesk_ticket_id"
312
+ ]
313
+ assert len(zendesk_blocks) == 1
314
+
315
+ seller_blocks = [
316
+ b
317
+ for b in view.blocks
318
+ if hasattr(b, "block_id") and b.block_id == "seller_contract_id"
319
+ ]
320
+ assert len(seller_blocks) == 1
321
+
322
+ def test_build_modal_fn_none_impacts_hides_all_fields(
323
+ self, priority_factory, impact_level_factory
324
+ ):
325
+ """All impacts at NONE level should not show any specific fields."""
326
+ priority_factory(value=1, default=True)
327
+ customer_impact = impact_level_factory(value=LevelChoices.NONE, impact__name="Customers")
328
+ seller_impact = impact_level_factory(value=LevelChoices.NONE, impact__name="Sellers")
329
+
330
+ modal = OpeningUnifiedModal()
331
+
332
+ open_incident_context = {
333
+ "response_type": "critical",
334
+ "impact_form_data": {
335
+ "customers_impact": customer_impact,
336
+ "sellers_impact": seller_impact,
337
+ },
338
+ "details_form_data": {},
339
+ }
340
+
341
+ view = modal.build_modal_fn(open_incident_context=open_incident_context)
342
+
343
+ # Should NOT show any specific fields
344
+ zendesk_blocks = [
345
+ b
346
+ for b in view.blocks
347
+ if hasattr(b, "block_id") and b.block_id == "zendesk_ticket_id"
348
+ ]
349
+ assert len(zendesk_blocks) == 0
350
+
351
+ seller_blocks = [
352
+ b
353
+ for b in view.blocks
354
+ if hasattr(b, "block_id") and b.block_id == "seller_contract_id"
355
+ ]
356
+ assert len(seller_blocks) == 0
357
+
358
+ def test_build_modal_fn_normal_with_customer_shows_team_and_zendesk(
359
+ self, priority_factory, impact_level_factory
360
+ ):
361
+ """P4-P5 with customer impact should show both feature team and Zendesk."""
362
+ priority_factory(value=4, default=True)
363
+ customer_impact = impact_level_factory(value=LevelChoices.HIGH, impact__name="Customers")
364
+
365
+ modal = OpeningUnifiedModal()
366
+
367
+ open_incident_context = {
368
+ "response_type": "normal",
369
+ "impact_form_data": {"customers_impact": customer_impact},
370
+ "details_form_data": {},
371
+ }
372
+
373
+ view = modal.build_modal_fn(open_incident_context=open_incident_context)
374
+
375
+ # Should show both fields
376
+ team_blocks = [
377
+ b
378
+ for b in view.blocks
379
+ if hasattr(b, "block_id") and b.block_id == "suggested_team_routing"
380
+ ]
381
+ assert len(team_blocks) == 1
382
+
383
+ zendesk_blocks = [
384
+ b
385
+ for b in view.blocks
386
+ if hasattr(b, "block_id") and b.block_id == "zendesk_ticket_id"
387
+ ]
388
+ assert len(zendesk_blocks) == 1
389
+
390
+ def test_handle_modal_fn_preserves_custom_fields_in_validation(
391
+ self, priority_factory, impact_level_factory
392
+ ):
393
+ """Form validation during submission should preserve custom fields based on impact context."""
394
+ # Setup
395
+ priority = priority_factory(value=1, default=True)
396
+ customer_impact = impact_level_factory(value=LevelChoices.HIGH, impact__name="Customers")
397
+
398
+ modal = OpeningUnifiedModal()
399
+
400
+ # Simulate private_metadata as it would be in the modal submission
401
+ private_metadata = {
402
+ "response_type": "critical",
403
+ "impact_form_data": {"customers_impact": str(customer_impact.id)},
404
+ "details_form_data": {"priority": str(priority.id)},
405
+ }
406
+
407
+ # This is what handle_modal_fn now does (with our fix)
408
+ # It passes open_incident_context to the form initialization
409
+ slack_form = modal.get_form_class()(
410
+ data={}, # Empty data for this test - we just want to verify field visibility
411
+ open_incident_context=private_metadata,
412
+ )
413
+
414
+ form = slack_form.form
415
+
416
+ # Verify that zendesk_ticket_id field exists in the form
417
+ # This proves the context was passed correctly and field visibility was configured
418
+ assert "zendesk_ticket_id" in form.fields, "zendesk_ticket_id should be present in form fields when customer impact is selected"
419
+
420
+ # Verify that suggested_team_routing is NOT in the form (critical incident)
421
+ assert "suggested_team_routing" not in form.fields, "suggested_team_routing should not be present for critical incidents"