firefighter-incident 0.0.13__py3-none-any.whl → 0.0.15__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 (136) hide show
  1. firefighter/_version.py +16 -3
  2. firefighter/api/serializers.py +17 -8
  3. firefighter/api/urls.py +8 -1
  4. firefighter/api/views/_base.py +1 -1
  5. firefighter/api/views/components.py +5 -5
  6. firefighter/api/views/incidents.py +9 -9
  7. firefighter/confluence/signals/incident_updated.py +2 -2
  8. firefighter/firefighter/settings/components/raid.py +3 -0
  9. firefighter/incidents/admin.py +24 -24
  10. firefighter/incidents/enums.py +22 -2
  11. firefighter/incidents/factories.py +14 -5
  12. firefighter/incidents/forms/close_incident.py +4 -4
  13. firefighter/incidents/forms/closure_reason.py +45 -0
  14. firefighter/incidents/forms/create_incident.py +4 -4
  15. firefighter/incidents/forms/unified_incident.py +406 -0
  16. firefighter/incidents/forms/update_status.py +91 -5
  17. firefighter/incidents/menus.py +2 -2
  18. firefighter/incidents/migrations/0005_enable_from_p1_to_p5_priority.py +7 -5
  19. firefighter/incidents/migrations/0009_update_sla.py +7 -5
  20. firefighter/incidents/migrations/0020_create_incident_category_model.py +64 -0
  21. firefighter/incidents/migrations/0021_copy_component_data_to_incident_category.py +57 -0
  22. firefighter/incidents/migrations/0022_add_incident_category_fields.py +34 -0
  23. firefighter/incidents/migrations/0023_populate_incident_category_references.py +57 -0
  24. firefighter/incidents/migrations/0024_remove_component_fields_and_model.py +26 -0
  25. firefighter/incidents/migrations/0025_make_incident_category_required.py +24 -0
  26. firefighter/incidents/migrations/0026_alter_incidentcategory_options_and_more.py +39 -0
  27. firefighter/incidents/migrations/0027_add_closure_fields.py +40 -0
  28. firefighter/incidents/migrations/0028_add_closure_reason_constraint.py +33 -0
  29. firefighter/incidents/migrations/0029_add_custom_fields_to_incident.py +22 -0
  30. firefighter/incidents/models/__init__.py +1 -1
  31. firefighter/incidents/models/group.py +1 -1
  32. firefighter/incidents/models/incident.py +47 -20
  33. firefighter/incidents/models/{component.py → incident_category.py} +30 -29
  34. firefighter/incidents/models/incident_update.py +3 -3
  35. firefighter/incidents/static/css/main.min.css +1 -1
  36. firefighter/incidents/tables.py +9 -9
  37. firefighter/incidents/templates/layouts/partials/incident_card.html +1 -1
  38. firefighter/incidents/templates/layouts/partials/incident_timeline.html +2 -2
  39. firefighter/incidents/templates/layouts/partials/status_pill.html +1 -1
  40. firefighter/incidents/templates/pages/{component_detail.html → incident_category_detail.html} +13 -13
  41. firefighter/incidents/templates/pages/{component_list.html → incident_category_list.html} +2 -2
  42. firefighter/incidents/templates/pages/incident_detail.html +3 -3
  43. firefighter/incidents/urls.py +6 -6
  44. firefighter/incidents/views/components/details.py +9 -9
  45. firefighter/incidents/views/components/list.py +9 -9
  46. firefighter/incidents/views/reports.py +5 -5
  47. firefighter/incidents/views/users/details.py +2 -2
  48. firefighter/incidents/views/views.py +7 -7
  49. firefighter/jira_app/client.py +1 -1
  50. firefighter/logging/custom_json_formatter.py +2 -1
  51. firefighter/pagerduty/tasks/trigger_oncall.py +1 -1
  52. firefighter/raid/admin.py +0 -11
  53. firefighter/raid/apps.py +9 -26
  54. firefighter/raid/client.py +5 -5
  55. firefighter/raid/forms.py +84 -213
  56. firefighter/raid/migrations/0003_delete_raidarea.py +16 -0
  57. firefighter/raid/models.py +2 -21
  58. firefighter/raid/serializers.py +5 -4
  59. firefighter/raid/service.py +29 -27
  60. firefighter/raid/signals/incident_created.py +42 -15
  61. firefighter/raid/signals/incident_updated.py +3 -2
  62. firefighter/raid/utils.py +1 -1
  63. firefighter/raid/views/__init__.py +1 -1
  64. firefighter/slack/admin.py +8 -8
  65. firefighter/slack/management/commands/switch_test_users.py +272 -0
  66. firefighter/slack/messages/slack_messages.py +24 -9
  67. firefighter/slack/migrations/0005_add_incident_categories_fields.py +33 -0
  68. firefighter/slack/migrations/0006_copy_components_to_incident_categories.py +57 -0
  69. firefighter/slack/migrations/0007_remove_components_fields.py +22 -0
  70. firefighter/slack/migrations/0008_alter_conversation_incident_categories_and_more.py +33 -0
  71. firefighter/slack/models/conversation.py +3 -3
  72. firefighter/slack/models/incident_channel.py +1 -1
  73. firefighter/slack/models/user.py +1 -1
  74. firefighter/slack/models/user_group.py +3 -3
  75. firefighter/slack/rules.py +2 -2
  76. firefighter/slack/signals/create_incident_conversation.py +6 -0
  77. firefighter/slack/signals/get_users.py +2 -2
  78. firefighter/slack/signals/incident_updated.py +8 -2
  79. firefighter/slack/utils.py +2 -2
  80. firefighter/slack/views/events/home.py +2 -2
  81. firefighter/slack/views/modals/__init__.py +4 -0
  82. firefighter/slack/views/modals/base_modal/form_utils.py +78 -0
  83. firefighter/slack/views/modals/close.py +18 -5
  84. firefighter/slack/views/modals/closure_reason.py +193 -0
  85. firefighter/slack/views/modals/open.py +83 -12
  86. firefighter/slack/views/modals/opening/check_current_incidents.py +2 -2
  87. firefighter/slack/views/modals/opening/details/unified.py +203 -0
  88. firefighter/slack/views/modals/opening/select_impact.py +5 -2
  89. firefighter/slack/views/modals/opening/set_details.py +3 -2
  90. firefighter/slack/views/modals/postmortem.py +10 -2
  91. firefighter/slack/views/modals/update_status.py +32 -6
  92. firefighter/slack/views/modals/utils.py +51 -0
  93. firefighter_fixtures/incidents/{components.json → incident_categories.json} +52 -52
  94. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/METADATA +2 -2
  95. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/RECORD +133 -88
  96. firefighter_tests/conftest.py +4 -5
  97. firefighter_tests/test_api/test_api_landbot.py +1 -1
  98. firefighter_tests/test_firefighter/test_sso.py +146 -0
  99. firefighter_tests/test_incidents/test_enums.py +100 -0
  100. firefighter_tests/test_incidents/test_forms/conftest.py +179 -0
  101. firefighter_tests/test_incidents/test_forms/test_closure_reason.py +91 -0
  102. firefighter_tests/test_incidents/test_forms/test_form_utils.py +15 -15
  103. firefighter_tests/test_incidents/test_forms/test_unified_incident_form.py +570 -0
  104. firefighter_tests/test_incidents/test_forms/test_unified_incident_form_integration.py +581 -0
  105. firefighter_tests/test_incidents/test_forms/test_unified_incident_form_p4_p5.py +410 -0
  106. firefighter_tests/test_incidents/test_forms/test_update_status_workflow.py +343 -0
  107. firefighter_tests/test_incidents/test_forms/test_workflow_transitions.py +167 -0
  108. firefighter_tests/test_incidents/test_incident_urls.py +3 -3
  109. firefighter_tests/test_incidents/test_models/test_incident_category.py +165 -0
  110. firefighter_tests/test_incidents/test_models/test_incident_model.py +70 -2
  111. firefighter_tests/test_raid/conftest.py +154 -0
  112. firefighter_tests/test_raid/test_p1_p3_jira_fields.py +372 -0
  113. firefighter_tests/test_raid/test_priority_mapping.py +267 -0
  114. firefighter_tests/test_raid/test_raid_client.py +580 -0
  115. firefighter_tests/test_raid/test_raid_forms.py +552 -0
  116. firefighter_tests/test_raid/test_raid_models.py +185 -0
  117. firefighter_tests/test_raid/test_raid_serializers.py +507 -0
  118. firefighter_tests/test_raid/test_raid_service.py +442 -0
  119. firefighter_tests/test_raid/test_raid_signals.py +187 -0
  120. firefighter_tests/test_raid/test_raid_views.py +196 -0
  121. firefighter_tests/test_slack/messages/__init__.py +0 -0
  122. firefighter_tests/test_slack/messages/test_slack_messages.py +367 -0
  123. firefighter_tests/test_slack/views/modals/conftest.py +140 -0
  124. firefighter_tests/test_slack/views/modals/test_close.py +71 -9
  125. firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +138 -0
  126. firefighter_tests/test_slack/views/modals/test_form_utils_multiple_choice.py +249 -0
  127. firefighter_tests/test_slack/views/modals/test_open.py +146 -2
  128. firefighter_tests/test_slack/views/modals/test_opening_unified.py +421 -0
  129. firefighter_tests/test_slack/views/modals/test_update_status.py +331 -7
  130. firefighter_tests/test_slack/views/modals/test_utils.py +135 -0
  131. firefighter/raid/views/open_normal.py +0 -139
  132. firefighter/slack/views/modals/opening/details/critical.py +0 -88
  133. firefighter_fixtures/raid/area.json +0 -1
  134. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/WHEEL +0 -0
  135. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/entry_points.txt +0 -0
  136. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,57 @@
1
+ # Generated manually on 2025-08-19 - Step 2: Copy Component data to IncidentCategory
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ def copy_component_data_to_incident_category(apps, schema_editor):
7
+ """Copy all data from Component to IncidentCategory"""
8
+ Component = apps.get_model("incidents", "Component")
9
+ IncidentCategory = apps.get_model("incidents", "IncidentCategory")
10
+
11
+ # Copy all components to incident categories with the same fields
12
+ for component in Component.objects.all():
13
+ IncidentCategory.objects.create(
14
+ id=component.id, # Keep same UUID
15
+ name=component.name,
16
+ description=component.description,
17
+ order=component.order,
18
+ private=component.private,
19
+ deploy_warning=component.deploy_warning,
20
+ created_at=component.created_at,
21
+ updated_at=component.updated_at,
22
+ group=component.group,
23
+ )
24
+
25
+
26
+ def reverse_copy_component_data_to_incident_category(apps, schema_editor):
27
+ """Reverse operation - copy IncidentCategory back to Component if needed"""
28
+ Component = apps.get_model("incidents", "Component")
29
+ IncidentCategory = apps.get_model("incidents", "IncidentCategory")
30
+
31
+ # This would only work if Component table still exists during rollback
32
+ for incident_category in IncidentCategory.objects.all():
33
+ Component.objects.create(
34
+ id=incident_category.id,
35
+ name=incident_category.name,
36
+ description=incident_category.description,
37
+ order=incident_category.order,
38
+ private=incident_category.private,
39
+ deploy_warning=incident_category.deploy_warning,
40
+ created_at=incident_category.created_at,
41
+ updated_at=incident_category.updated_at,
42
+ group=incident_category.group,
43
+ )
44
+
45
+
46
+ class Migration(migrations.Migration):
47
+
48
+ dependencies = [
49
+ ("incidents", "0020_create_incident_category_model"),
50
+ ]
51
+
52
+ operations = [
53
+ migrations.RunPython(
54
+ copy_component_data_to_incident_category,
55
+ reverse_copy_component_data_to_incident_category,
56
+ ),
57
+ ]
@@ -0,0 +1,34 @@
1
+ # Generated manually on 2025-08-19 - Step 3: Add incident_category fields (nullable initially)
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ("incidents", "0021_copy_component_data_to_incident_category"),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddField(
15
+ model_name="incident",
16
+ name="incident_category",
17
+ field=models.ForeignKey(
18
+ blank=True,
19
+ null=True,
20
+ on_delete=django.db.models.deletion.PROTECT,
21
+ to="incidents.incidentcategory",
22
+ ),
23
+ ),
24
+ migrations.AddField(
25
+ model_name="incidentupdate",
26
+ name="incident_category",
27
+ field=models.ForeignKey(
28
+ blank=True,
29
+ null=True,
30
+ on_delete=django.db.models.deletion.SET_NULL,
31
+ to="incidents.incidentcategory",
32
+ ),
33
+ ),
34
+ ]
@@ -0,0 +1,57 @@
1
+ # Generated manually on 2025-08-19 - Step 4: Populate incident_category references from component
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ def populate_incident_category_references(apps, schema_editor):
7
+ """Copy component references to incident_category references"""
8
+ Incident = apps.get_model("incidents", "Incident")
9
+ IncidentUpdate = apps.get_model("incidents", "IncidentUpdate")
10
+
11
+ # Update all incidents to point to the corresponding incident category
12
+ incidents_updated = 0
13
+ for incident in Incident.objects.select_related("component").all():
14
+ if incident.component:
15
+ # Find the corresponding incident category (same UUID)
16
+ incident.incident_category_id = incident.component_id
17
+ incident.save(update_fields=["incident_category"])
18
+ incidents_updated += 1
19
+
20
+ # Update all incident updates to point to the corresponding incident category
21
+ updates_updated = 0
22
+ for incident_update in IncidentUpdate.objects.select_related("component").all():
23
+ if incident_update.component:
24
+ incident_update.incident_category_id = incident_update.component_id
25
+ incident_update.save(update_fields=["incident_category"])
26
+ updates_updated += 1
27
+
28
+
29
+ def reverse_populate_incident_category_references(apps, schema_editor):
30
+ """Reverse: copy incident_category references back to component references"""
31
+ Incident = apps.get_model("incidents", "Incident")
32
+ IncidentUpdate = apps.get_model("incidents", "IncidentUpdate")
33
+
34
+ # Restore component references from incident_category references
35
+ for incident in Incident.objects.select_related("incident_category").all():
36
+ if incident.incident_category:
37
+ incident.component_id = incident.incident_category_id
38
+ incident.save(update_fields=["component"])
39
+
40
+ for incident_update in IncidentUpdate.objects.select_related("incident_category").all():
41
+ if incident_update.incident_category:
42
+ incident_update.component_id = incident_update.incident_category_id
43
+ incident_update.save(update_fields=["component"])
44
+
45
+
46
+ class Migration(migrations.Migration):
47
+
48
+ dependencies = [
49
+ ("incidents", "0022_add_incident_category_fields"),
50
+ ]
51
+
52
+ operations = [
53
+ migrations.RunPython(
54
+ populate_incident_category_references,
55
+ reverse_populate_incident_category_references,
56
+ ),
57
+ ]
@@ -0,0 +1,26 @@
1
+ # Generated manually on 2025-08-19 - Step 5: Remove component fields and model
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("incidents", "0023_populate_incident_category_references"),
10
+ ]
11
+
12
+ operations = [
13
+ # Remove the component foreign key fields
14
+ migrations.RemoveField(
15
+ model_name="incident",
16
+ name="component",
17
+ ),
18
+ migrations.RemoveField(
19
+ model_name="incidentupdate",
20
+ name="component",
21
+ ),
22
+ # Remove the component model entirely
23
+ migrations.DeleteModel(
24
+ name="Component",
25
+ ),
26
+ ]
@@ -0,0 +1,24 @@
1
+ # Generated manually on 2025-08-19 - Step 6: Make incident_category field required
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ("incidents", "0024_remove_component_fields_and_model"),
11
+ ("slack", "0007_remove_components_fields"),
12
+ ]
13
+
14
+ operations = [
15
+ # Make incident_category field required (not null)
16
+ migrations.AlterField(
17
+ model_name="incident",
18
+ name="incident_category",
19
+ field=models.ForeignKey(
20
+ on_delete=django.db.models.deletion.PROTECT,
21
+ to="incidents.incidentcategory",
22
+ ),
23
+ ),
24
+ ]
@@ -0,0 +1,39 @@
1
+ # Generated by Django 4.2.23 on 2025-09-17 17:49
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("incidents", "0025_make_incident_category_required"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterModelOptions(
14
+ name="incidentcategory",
15
+ options={
16
+ "ordering": ["order"],
17
+ "verbose_name_plural": "incident categories",
18
+ },
19
+ ),
20
+ migrations.AlterField(
21
+ model_name="impactlevel",
22
+ name="description",
23
+ field=models.TextField(
24
+ blank=True,
25
+ help_text="Detailed multi-line description for this impact level.",
26
+ null=True,
27
+ ),
28
+ ),
29
+ migrations.AlterField(
30
+ model_name="impactlevel",
31
+ name="emoji",
32
+ field=models.CharField(default="▶", max_length=5),
33
+ ),
34
+ migrations.AlterField(
35
+ model_name="impacttype",
36
+ name="emoji",
37
+ field=models.CharField(default="▶", max_length=5),
38
+ ),
39
+ ]
@@ -0,0 +1,40 @@
1
+ # Generated by Django 4.2.23 on 2025-09-26 12:47
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("incidents", "0026_alter_incidentcategory_options_and_more"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="incident",
15
+ name="closure_reason",
16
+ field=models.CharField(
17
+ blank=True,
18
+ choices=[
19
+ ("resolved", "Resolved normally"),
20
+ ("duplicate", "Duplicate incident"),
21
+ ("false_positive", "False alarm - no actual issue"),
22
+ ("superseded", "Superseded by another incident"),
23
+ ("external", "External dependency/known issue"),
24
+ ("cancelled", "Cancelled - no longer relevant"),
25
+ ],
26
+ help_text="Reason for direct incident closure bypassing normal workflow",
27
+ max_length=50,
28
+ null=True,
29
+ ),
30
+ ),
31
+ migrations.AddField(
32
+ model_name="incident",
33
+ name="closure_reference",
34
+ field=models.CharField(
35
+ blank=True,
36
+ help_text="Reference incident ID or external link for closure context",
37
+ max_length=100,
38
+ ),
39
+ ),
40
+ ]
@@ -0,0 +1,33 @@
1
+ # Generated by Django 4.2.23 on 2025-09-26 12:47
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("incidents", "0027_add_closure_fields"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddConstraint(
14
+ model_name="incident",
15
+ constraint=models.CheckConstraint(
16
+ check=models.Q(
17
+ (
18
+ "closure_reason__in",
19
+ [
20
+ "resolved",
21
+ "duplicate",
22
+ "false_positive",
23
+ "superseded",
24
+ "external",
25
+ "cancelled",
26
+ None,
27
+ ],
28
+ )
29
+ ),
30
+ name="incidents_incident_closure_reason_valid",
31
+ ),
32
+ ),
33
+ ]
@@ -0,0 +1,22 @@
1
+ # Generated by Django 4.2.21 on 2025-10-13 13:55
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("incidents", "0028_add_closure_reason_constraint"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="incident",
15
+ name="custom_fields",
16
+ field=models.JSONField(
17
+ blank=True,
18
+ default=dict,
19
+ help_text="Custom fields for incident (zendesk_ticket_id, seller_contract_id, etc.)",
20
+ ),
21
+ ),
22
+ ]
@@ -1,10 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
- from firefighter.incidents.models.component import Component
4
3
  from firefighter.incidents.models.environment import Environment
5
4
  from firefighter.incidents.models.group import Group
6
5
  from firefighter.incidents.models.impact import Impact
7
6
  from firefighter.incidents.models.incident import Incident
7
+ from firefighter.incidents.models.incident_category import IncidentCategory
8
8
  from firefighter.incidents.models.incident_cost import IncidentCost
9
9
  from firefighter.incidents.models.incident_cost_type import IncidentCostType
10
10
  from firefighter.incidents.models.incident_role_type import IncidentRoleType
@@ -7,7 +7,7 @@ from django.db.models.manager import Manager
7
7
 
8
8
 
9
9
  class Group(models.Model):
10
- """Group of [firefighter.incidents.models.component.Component]. Not a group of users."""
10
+ """Group of [firefighter.incidents.models.incident_category.IncidentCategory]. Not a group of users."""
11
11
  id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
12
12
  name = models.CharField(max_length=128, unique=True)
13
13
  description = models.TextField(blank=True)
@@ -29,10 +29,10 @@ from firefighter.firefighter.fields_forms_widgets import (
29
29
  GroupedCheckboxSelectMultiple,
30
30
  )
31
31
  from firefighter.incidents import signals
32
- from firefighter.incidents.enums import IncidentStatus
33
- from firefighter.incidents.models.component import Component
32
+ from firefighter.incidents.enums import ClosureReason, IncidentStatus
34
33
  from firefighter.incidents.models.environment import Environment
35
34
  from firefighter.incidents.models.group import Group
35
+ from firefighter.incidents.models.incident_category import IncidentCategory
36
36
  from firefighter.incidents.models.incident_membership import (
37
37
  IncidentMembership,
38
38
  IncidentRole,
@@ -90,7 +90,7 @@ class IncidentManager(models.Manager["Incident"]):
90
90
  """
91
91
  with transaction.atomic():
92
92
  if "private" not in kwargs:
93
- kwargs["private"] = kwargs["component"].private
93
+ kwargs["private"] = kwargs["incident_category"].private
94
94
  if "severity" not in kwargs and "priority" in kwargs:
95
95
  kwargs["severity"] = Severity.objects.get(
96
96
  value=kwargs["priority"].value
@@ -110,7 +110,7 @@ class IncidentManager(models.Manager["Incident"]):
110
110
  description=incident.description,
111
111
  status=incident.status, # type: ignore[misc]
112
112
  priority=incident.priority,
113
- component=incident.component,
113
+ incident_category=incident.incident_category,
114
114
  created_by=incident.created_by,
115
115
  commander=incident.created_by,
116
116
  incident=incident,
@@ -212,8 +212,8 @@ class Incident(models.Model):
212
212
  on_delete=models.PROTECT,
213
213
  help_text="Priority",
214
214
  )
215
- component = models.ForeignKey(
216
- Component, on_delete=models.PROTECT
215
+ incident_category = models.ForeignKey(
216
+ IncidentCategory, on_delete=models.PROTECT
217
217
  )
218
218
  environment = models.ForeignKey(
219
219
  Environment, on_delete=models.PROTECT
@@ -230,6 +230,24 @@ class Incident(models.Model):
230
230
  closed_at = models.DateTimeField(
231
231
  null=True, blank=True
232
232
  ) # XXX-ZOR make this an event
233
+ closure_reason = models.CharField(
234
+ max_length=50,
235
+ choices=ClosureReason.choices,
236
+ null=True,
237
+ blank=True,
238
+ help_text="Reason for direct incident closure bypassing normal workflow",
239
+ )
240
+ closure_reference = models.CharField(
241
+ max_length=100,
242
+ blank=True,
243
+ help_text="Reference incident ID or external link for closure context",
244
+ )
245
+
246
+ custom_fields = models.JSONField(
247
+ default=dict,
248
+ blank=True,
249
+ help_text="Custom fields for incident (zendesk_ticket_id, seller_contract_id, etc.)",
250
+ )
233
251
 
234
252
  # XXX-ZOR pick a more meaningful name. maybe 'hidden'
235
253
  # XXX-ZOR document intent and impl. status
@@ -268,7 +286,11 @@ class Incident(models.Model):
268
286
  models.CheckConstraint(
269
287
  name="%(app_label)s_%(class)s__status_valid",
270
288
  check=models.Q(_status__in=IncidentStatus.values),
271
- )
289
+ ),
290
+ models.CheckConstraint(
291
+ name="%(app_label)s_%(class)s_closure_reason_valid",
292
+ check=models.Q(closure_reason__in=[*ClosureReason.values, None]),
293
+ ),
272
294
  ]
273
295
 
274
296
  def __str__(self) -> str:
@@ -331,6 +353,11 @@ class Incident(models.Model):
331
353
  def can_be_closed(self) -> tuple[bool, list[tuple[str, str]]]:
332
354
  # XXX-ZOR we should use a proper FSM abstraction
333
355
  cant_closed_reasons: list[tuple[str, str]] = []
356
+
357
+ # Allow direct closure when closure_reason is provided (bypasses normal workflow)
358
+ if self.closure_reason:
359
+ return True, []
360
+
334
361
  if self.ignore:
335
362
  return True, []
336
363
  if self.needs_postmortem:
@@ -341,11 +368,11 @@ class Incident(models.Model):
341
368
  f"Incident is not in PostMortem status, and needs one because of its priority and environment ({self.priority.name}/{self.environment.value}).",
342
369
  )
343
370
  )
344
- elif self.status.value < IncidentStatus.FIXED:
371
+ elif self.status.value < IncidentStatus.MITIGATED:
345
372
  cant_closed_reasons.append(
346
373
  (
347
374
  "STATUS_NOT_MITIGATED",
348
- f"Incident is not in {IncidentStatus.FIXED.label} status (currently {self.status.label}).",
375
+ f"Incident is not in {IncidentStatus.MITIGATED.label} status (currently {self.status.label}).",
349
376
  )
350
377
  )
351
378
  missing_milestones = self.missing_milestones()
@@ -545,7 +572,7 @@ class Incident(models.Model):
545
572
  message: str | None = None,
546
573
  status: int | None = None,
547
574
  priority_id: UUID | None = None,
548
- component_id: UUID | None = None,
575
+ incident_category_id: UUID | None = None,
549
576
  created_by: User | None = None,
550
577
  event_type: str | None = None,
551
578
  title: str | None = None,
@@ -564,7 +591,7 @@ class Incident(models.Model):
564
591
 
565
592
  _update_incident_field(self, "_status", status, updated_fields)
566
593
  _update_incident_field(self, "priority_id", priority_id, updated_fields)
567
- _update_incident_field(self, "component_id", component_id, updated_fields)
594
+ _update_incident_field(self, "incident_category_id", incident_category_id, updated_fields)
568
595
  _update_incident_field(self, "title", title, updated_fields)
569
596
  _update_incident_field(self, "description", description, updated_fields)
570
597
  _update_incident_field(self, "environment_id", environment_id, updated_fields)
@@ -582,7 +609,7 @@ class Incident(models.Model):
582
609
  status=status, # type: ignore
583
610
  priority_id=priority_id,
584
611
  environment_id=environment_id,
585
- component_id=component_id,
612
+ incident_category_id=incident_category_id,
586
613
  message=message,
587
614
  created_by=created_by,
588
615
  title=title,
@@ -592,7 +619,7 @@ class Incident(models.Model):
592
619
  )
593
620
  incident_update.save()
594
621
 
595
- if status == IncidentStatus.FIXED:
622
+ if status == IncidentStatus.MITIGATED:
596
623
  IncidentUpdate.objects.update_or_create(
597
624
  incident_id=self.id,
598
625
  event_type="recovered",
@@ -628,12 +655,12 @@ class Incident(models.Model):
628
655
  incidentupdate_set: QuerySet[IncidentUpdate]
629
656
 
630
657
 
631
- def component_filter_choices_queryset(_: Any) -> QuerySet[Component]:
632
- """Queryset for choices of Components in IncidentFilterSet.
658
+ def incident_category_filter_choices_queryset(_: Any) -> QuerySet[IncidentCategory]:
659
+ """Queryset for choices of IncidentCategories in IncidentFilterSet.
633
660
  Moved it as a function because models are not loaded when creating filters.
634
661
  """
635
662
  return (
636
- Component.objects.all()
663
+ IncidentCategory.objects.all()
637
664
  .select_related("group")
638
665
  .order_by(
639
666
  "group__order",
@@ -663,11 +690,11 @@ class IncidentFilterSet(django_filters.FilterSet):
663
690
  widget=CustomCheckboxSelectMultiple,
664
691
  )
665
692
  group = ModelMultipleChoiceFilter(
666
- queryset=Group.objects.all(), field_name="component__group_id", label="Group"
693
+ queryset=Group.objects.all(), field_name="incident_category__group_id", label="Group"
667
694
  )
668
- component = ModelMultipleChoiceFilter(
669
- queryset=component_filter_choices_queryset,
670
- label="Issue category",
695
+ incident_category = ModelMultipleChoiceFilter(
696
+ queryset=incident_category_filter_choices_queryset,
697
+ label="Incident category",
671
698
  widget=GroupedCheckboxSelectMultiple,
672
699
  )
673
700
  created_at = FFDateRangeSingleFilter(field_name="created_at")