firefighter-incident 0.0.13__py3-none-any.whl → 0.0.14__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 (99) hide show
  1. firefighter/_version.py +16 -3
  2. firefighter/api/serializers.py +8 -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/firefighter/settings/components/raid.py +3 -0
  8. firefighter/incidents/admin.py +24 -24
  9. firefighter/incidents/factories.py +14 -5
  10. firefighter/incidents/forms/close_incident.py +4 -4
  11. firefighter/incidents/forms/create_incident.py +4 -4
  12. firefighter/incidents/forms/update_status.py +4 -4
  13. firefighter/incidents/menus.py +2 -2
  14. firefighter/incidents/migrations/0005_enable_from_p1_to_p5_priority.py +7 -5
  15. firefighter/incidents/migrations/0009_update_sla.py +7 -5
  16. firefighter/incidents/migrations/0020_create_incident_category_model.py +64 -0
  17. firefighter/incidents/migrations/0021_copy_component_data_to_incident_category.py +57 -0
  18. firefighter/incidents/migrations/0022_add_incident_category_fields.py +34 -0
  19. firefighter/incidents/migrations/0023_populate_incident_category_references.py +57 -0
  20. firefighter/incidents/migrations/0024_remove_component_fields_and_model.py +26 -0
  21. firefighter/incidents/migrations/0025_make_incident_category_required.py +24 -0
  22. firefighter/incidents/migrations/0026_alter_incidentcategory_options_and_more.py +39 -0
  23. firefighter/incidents/models/__init__.py +1 -1
  24. firefighter/incidents/models/group.py +1 -1
  25. firefighter/incidents/models/incident.py +15 -15
  26. firefighter/incidents/models/{component.py → incident_category.py} +30 -29
  27. firefighter/incidents/models/incident_update.py +3 -3
  28. firefighter/incidents/tables.py +9 -9
  29. firefighter/incidents/templates/layouts/partials/incident_card.html +1 -1
  30. firefighter/incidents/templates/layouts/partials/incident_timeline.html +2 -2
  31. firefighter/incidents/templates/pages/{component_detail.html → incident_category_detail.html} +13 -13
  32. firefighter/incidents/templates/pages/{component_list.html → incident_category_list.html} +2 -2
  33. firefighter/incidents/templates/pages/incident_detail.html +3 -3
  34. firefighter/incidents/urls.py +6 -6
  35. firefighter/incidents/views/components/details.py +9 -9
  36. firefighter/incidents/views/components/list.py +9 -9
  37. firefighter/incidents/views/reports.py +2 -2
  38. firefighter/incidents/views/users/details.py +2 -2
  39. firefighter/incidents/views/views.py +7 -7
  40. firefighter/jira_app/client.py +1 -1
  41. firefighter/logging/custom_json_formatter.py +2 -1
  42. firefighter/pagerduty/tasks/trigger_oncall.py +1 -1
  43. firefighter/raid/admin.py +0 -11
  44. firefighter/raid/client.py +3 -3
  45. firefighter/raid/forms.py +53 -19
  46. firefighter/raid/migrations/0003_delete_raidarea.py +16 -0
  47. firefighter/raid/models.py +2 -21
  48. firefighter/raid/serializers.py +5 -4
  49. firefighter/raid/service.py +29 -27
  50. firefighter/raid/signals/incident_created.py +4 -2
  51. firefighter/raid/utils.py +1 -1
  52. firefighter/raid/views/__init__.py +1 -1
  53. firefighter/raid/views/open_normal.py +2 -2
  54. firefighter/slack/admin.py +8 -8
  55. firefighter/slack/management/commands/switch_test_users.py +272 -0
  56. firefighter/slack/messages/slack_messages.py +5 -5
  57. firefighter/slack/migrations/0005_add_incident_categories_fields.py +33 -0
  58. firefighter/slack/migrations/0006_copy_components_to_incident_categories.py +57 -0
  59. firefighter/slack/migrations/0007_remove_components_fields.py +22 -0
  60. firefighter/slack/migrations/0008_alter_conversation_incident_categories_and_more.py +33 -0
  61. firefighter/slack/models/conversation.py +3 -3
  62. firefighter/slack/models/incident_channel.py +1 -1
  63. firefighter/slack/models/user.py +1 -1
  64. firefighter/slack/models/user_group.py +3 -3
  65. firefighter/slack/rules.py +1 -1
  66. firefighter/slack/signals/get_users.py +2 -2
  67. firefighter/slack/signals/incident_updated.py +1 -1
  68. firefighter/slack/utils.py +2 -2
  69. firefighter/slack/views/events/home.py +2 -2
  70. firefighter/slack/views/modals/base_modal/form_utils.py +15 -0
  71. firefighter/slack/views/modals/close.py +3 -3
  72. firefighter/slack/views/modals/open.py +25 -1
  73. firefighter/slack/views/modals/opening/check_current_incidents.py +2 -2
  74. firefighter/slack/views/modals/opening/details/critical.py +1 -1
  75. firefighter/slack/views/modals/opening/select_impact.py +5 -2
  76. firefighter/slack/views/modals/update_status.py +4 -4
  77. firefighter_fixtures/incidents/{components.json → incident_categories.json} +52 -52
  78. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.14.dist-info}/METADATA +2 -2
  79. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.14.dist-info}/RECORD +98 -77
  80. firefighter_tests/conftest.py +4 -5
  81. firefighter_tests/test_api/test_api_landbot.py +1 -1
  82. firefighter_tests/test_firefighter/test_sso.py +146 -0
  83. firefighter_tests/test_incidents/test_forms/test_form_utils.py +15 -15
  84. firefighter_tests/test_incidents/test_incident_urls.py +3 -3
  85. firefighter_tests/test_incidents/test_models/test_incident_category.py +165 -0
  86. firefighter_tests/test_incidents/test_models/test_incident_model.py +2 -2
  87. firefighter_tests/test_raid/test_priority_mapping.py +267 -0
  88. firefighter_tests/test_raid/test_raid_client.py +580 -0
  89. firefighter_tests/test_raid/test_raid_forms.py +795 -0
  90. firefighter_tests/test_raid/test_raid_models.py +185 -0
  91. firefighter_tests/test_raid/test_raid_serializers.py +507 -0
  92. firefighter_tests/test_raid/test_raid_service.py +442 -0
  93. firefighter_tests/test_raid/test_raid_views.py +196 -0
  94. firefighter_tests/test_slack/views/modals/test_close.py +6 -6
  95. firefighter_tests/test_slack/views/modals/test_update_status.py +4 -4
  96. firefighter_fixtures/raid/area.json +0 -1
  97. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.14.dist-info}/WHEEL +0 -0
  98. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.14.dist-info}/entry_points.txt +0 -0
  99. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.14.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
+ ]
@@ -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)
@@ -30,9 +30,9 @@ from firefighter.firefighter.fields_forms_widgets import (
30
30
  )
31
31
  from firefighter.incidents import signals
32
32
  from firefighter.incidents.enums import IncidentStatus
33
- from firefighter.incidents.models.component import Component
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
@@ -545,7 +545,7 @@ class Incident(models.Model):
545
545
  message: str | None = None,
546
546
  status: int | None = None,
547
547
  priority_id: UUID | None = None,
548
- component_id: UUID | None = None,
548
+ incident_category_id: UUID | None = None,
549
549
  created_by: User | None = None,
550
550
  event_type: str | None = None,
551
551
  title: str | None = None,
@@ -564,7 +564,7 @@ class Incident(models.Model):
564
564
 
565
565
  _update_incident_field(self, "_status", status, updated_fields)
566
566
  _update_incident_field(self, "priority_id", priority_id, updated_fields)
567
- _update_incident_field(self, "component_id", component_id, updated_fields)
567
+ _update_incident_field(self, "incident_category_id", incident_category_id, updated_fields)
568
568
  _update_incident_field(self, "title", title, updated_fields)
569
569
  _update_incident_field(self, "description", description, updated_fields)
570
570
  _update_incident_field(self, "environment_id", environment_id, updated_fields)
@@ -582,7 +582,7 @@ class Incident(models.Model):
582
582
  status=status, # type: ignore
583
583
  priority_id=priority_id,
584
584
  environment_id=environment_id,
585
- component_id=component_id,
585
+ incident_category_id=incident_category_id,
586
586
  message=message,
587
587
  created_by=created_by,
588
588
  title=title,
@@ -628,12 +628,12 @@ class Incident(models.Model):
628
628
  incidentupdate_set: QuerySet[IncidentUpdate]
629
629
 
630
630
 
631
- def component_filter_choices_queryset(_: Any) -> QuerySet[Component]:
632
- """Queryset for choices of Components in IncidentFilterSet.
631
+ def incident_category_filter_choices_queryset(_: Any) -> QuerySet[IncidentCategory]:
632
+ """Queryset for choices of IncidentCategories in IncidentFilterSet.
633
633
  Moved it as a function because models are not loaded when creating filters.
634
634
  """
635
635
  return (
636
- Component.objects.all()
636
+ IncidentCategory.objects.all()
637
637
  .select_related("group")
638
638
  .order_by(
639
639
  "group__order",
@@ -663,11 +663,11 @@ class IncidentFilterSet(django_filters.FilterSet):
663
663
  widget=CustomCheckboxSelectMultiple,
664
664
  )
665
665
  group = ModelMultipleChoiceFilter(
666
- queryset=Group.objects.all(), field_name="component__group_id", label="Group"
666
+ queryset=Group.objects.all(), field_name="incident_category__group_id", label="Group"
667
667
  )
668
- component = ModelMultipleChoiceFilter(
669
- queryset=component_filter_choices_queryset,
670
- label="Issue category",
668
+ incident_category = ModelMultipleChoiceFilter(
669
+ queryset=incident_category_filter_choices_queryset,
670
+ label="Incident category",
671
671
  widget=GroupedCheckboxSelectMultiple,
672
672
  )
673
673
  created_at = FFDateRangeSingleFilter(field_name="created_at")
@@ -40,18 +40,18 @@ logger = logging.getLogger(__name__)
40
40
  TZ = timezone.get_current_timezone()
41
41
 
42
42
 
43
- class ComponentManager(models.Manager["Component"]):
44
- model: type[Component]
43
+ class IncidentCategoryManager(models.Manager["IncidentCategory"]):
44
+ model: type[IncidentCategory]
45
45
 
46
46
  def queryset_with_mtbf(
47
47
  self,
48
48
  date_from: datetime,
49
49
  date_to: datetime,
50
- queryset: QuerySet[Component] | None = None,
50
+ queryset: QuerySet[IncidentCategory] | None = None,
51
51
  metric_type: str = "time_to_fix",
52
52
  field_name: str = "mtbf",
53
- ) -> QuerySet[Component]:
54
- """Returns a queryset of components with an additional `mtbf` field."""
53
+ ) -> QuerySet[IncidentCategory]:
54
+ """Returns a queryset of incident categories with an additional `mtbf` field."""
55
55
  date_to = min(date_to, datetime.now(tz=TZ))
56
56
 
57
57
  date_interval = date_to - date_from
@@ -62,12 +62,12 @@ class ComponentManager(models.Manager["Component"]):
62
62
  .annotate(
63
63
  metric_subquery=Subquery(
64
64
  IncidentMetric.objects.filter(
65
- incident__component=OuterRef("pk"),
65
+ incident__incident_category=OuterRef("pk"),
66
66
  metric_type__type=metric_type,
67
67
  incident__created_at__gte=date_from,
68
68
  incident__created_at__lte=date_to,
69
69
  )
70
- .values("incident__component")
70
+ .values("incident__incident_category")
71
71
  .annotate(sum_downtime=Sum("duration"))
72
72
  .values("sum_downtime")
73
73
  )
@@ -95,11 +95,11 @@ class ComponentManager(models.Manager["Component"]):
95
95
 
96
96
  @staticmethod
97
97
  def search(
98
- queryset: QuerySet[Component] | None, search_term: str
99
- ) -> tuple[QuerySet[Component], bool]:
98
+ queryset: QuerySet[IncidentCategory] | None, search_term: str
99
+ ) -> tuple[QuerySet[IncidentCategory], bool]:
100
100
  # XXX Common search method
101
101
  if queryset is None:
102
- queryset = Component.objects.all()
102
+ queryset = IncidentCategory.objects.all()
103
103
 
104
104
  # If not search, return the original queryset
105
105
  if search_term is None or search_term.strip() == "":
@@ -135,24 +135,24 @@ class ComponentManager(models.Manager["Component"]):
135
135
  return queryset, False
136
136
 
137
137
 
138
- class Component(models.Model):
139
- objects: ComponentManager = ComponentManager()
138
+ class IncidentCategory(models.Model):
139
+ objects: IncidentCategoryManager = IncidentCategoryManager()
140
140
 
141
141
  id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
142
142
  name = models.CharField(max_length=128)
143
143
  description = models.TextField(blank=True)
144
144
  order = models.IntegerField(
145
145
  default=0,
146
- help_text="Order of the component in the list. Should be unique per `Group`.",
146
+ help_text="Order of the incident category in the list. Should be unique per `Group`.",
147
147
  )
148
148
  group = models.ForeignKey(Group, on_delete=models.PROTECT)
149
149
  private = models.BooleanField(
150
150
  default=False,
151
- help_text="If true, incident created with this component won't be communicated, and conversations will be made private. This is useful for sensitive components. In the future, private incidents may be visible only to its members.",
151
+ help_text="If true, incident created with this incident category won't be communicated, and conversations will be made private. This is useful for sensitive incident categories. In the future, private incidents may be visible only to its members.",
152
152
  )
153
153
  deploy_warning = models.BooleanField(
154
154
  default=True,
155
- help_text="If true, a warning will be sent when creating an incident of high severity with this component.",
155
+ help_text="If true, a warning will be sent when creating an incident of high severity with this incident category.",
156
156
  )
157
157
 
158
158
  created_at = models.DateTimeField(auto_now_add=True)
@@ -169,16 +169,17 @@ class Component(models.Model):
169
169
 
170
170
  class Meta(TypedModelMeta):
171
171
  ordering = ["order"]
172
+ verbose_name_plural = "incident categories"
172
173
 
173
174
  def __str__(self) -> str:
174
175
  return f"{'🔒 ' if self.private else ''}{self.name}"
175
176
 
176
177
  def get_absolute_url(self) -> str:
177
- return reverse("incidents:component-detail", kwargs={"component_id": self.id})
178
+ return reverse("incidents:incident-category-detail", kwargs={"incident_category_id": self.id})
178
179
 
179
180
 
180
- class ComponentFilterSet(django_filters.FilterSet):
181
- """Set of filters for Component, share by Web UI and API."""
181
+ class IncidentCategoryFilterSet(django_filters.FilterSet):
182
+ """Set of filters for IncidentCategory, share by Web UI and API."""
182
183
 
183
184
  id = django_filters.CharFilter(lookup_expr="iexact")
184
185
  private = django_filters.BooleanFilter()
@@ -194,30 +195,30 @@ class ComponentFilterSet(django_filters.FilterSet):
194
195
  label="MTBF period",
195
196
  )
196
197
  search = django_filters.CharFilter(
197
- field_name="search", method="component_search", label="Search"
198
+ field_name="search", method="incident_category_search", label="Search"
198
199
  )
199
200
 
200
201
  @staticmethod
201
- def component_search(
202
- queryset: QuerySet[Component], _name: str, value: str
203
- ) -> QuerySet[Component]:
204
- """Search incidents by title, description, and ID.
202
+ def incident_category_search(
203
+ queryset: QuerySet[IncidentCategory], _name: str, value: str
204
+ ) -> QuerySet[IncidentCategory]:
205
+ """Search incident categories by title, description, and ID.
205
206
 
206
207
  Args:
207
- queryset (QuerySet[Component]): Queryset to search in.
208
+ queryset (QuerySet[IncidentCategory]): Queryset to search in.
208
209
  _name:
209
210
  value (str): Value to search for.
210
211
 
211
212
  Returns:
212
- QuerySet[Component]: Search results.
213
+ QuerySet[IncidentCategory]: Search results.
213
214
  """
214
- return Component.objects.search(queryset=queryset, search_term=value)[0]
215
+ return IncidentCategory.objects.search(queryset=queryset, search_term=value)[0]
215
216
 
216
217
  @staticmethod
217
218
  def metrics_period_filter(
218
- queryset: QuerySet[Component],
219
+ queryset: QuerySet[IncidentCategory],
219
220
  _name: str,
220
221
  value: tuple[datetime, datetime, Any, Any],
221
- ) -> QuerySet[Component]:
222
+ ) -> QuerySet[IncidentCategory]:
222
223
  gte, lte, _, _ = value
223
- return Component.objects.queryset_with_mtbf(gte, lte, queryset=queryset)
224
+ return IncidentCategory.objects.queryset_with_mtbf(gte, lte, queryset=queryset)
@@ -11,8 +11,8 @@ from django.utils import timezone
11
11
  from django_stubs_ext.db.models import TypedModelMeta
12
12
 
13
13
  from firefighter.incidents.enums import IncidentStatus
14
- from firefighter.incidents.models.component import Component
15
14
  from firefighter.incidents.models.environment import Environment
15
+ from firefighter.incidents.models.incident_category import IncidentCategory
16
16
  from firefighter.incidents.models.priority import Priority
17
17
  from firefighter.incidents.models.severity import Severity
18
18
  from firefighter.incidents.models.user import User
@@ -75,8 +75,8 @@ class IncidentUpdate(models.Model):
75
75
  incident = models.ForeignKey(
76
76
  "Incident", on_delete=models.CASCADE
77
77
  )
78
- component = models.ForeignKey(
79
- Component, null=True, blank=True, on_delete=models.SET_NULL
78
+ incident_category = models.ForeignKey(
79
+ IncidentCategory, null=True, blank=True, on_delete=models.SET_NULL
80
80
  )
81
81
  created_by = models.ForeignKey(
82
82
  User, null=True, blank=True, on_delete=models.SET_NULL
@@ -6,7 +6,7 @@ import django_tables2 as tables
6
6
  from django.conf import settings
7
7
 
8
8
  from firefighter.firefighter.tables_utils import BASE_TABLE_ATTRS
9
- from firefighter.incidents.models import Component, Incident
9
+ from firefighter.incidents.models import Incident, IncidentCategory
10
10
  from firefighter.incidents.models.incident import IncidentStatus
11
11
 
12
12
  if TYPE_CHECKING:
@@ -23,8 +23,8 @@ class IncidentTable(tables.Table):
23
23
  "priority",
24
24
  "status",
25
25
  "environment",
26
- "component",
27
- "component__group",
26
+ "incident_category",
27
+ "incident_category__group",
28
28
  "created_at",
29
29
  )
30
30
  order_by = "-id"
@@ -63,13 +63,13 @@ class IncidentTable(tables.Table):
63
63
  attrs={"td": {"class": "table-td text-center"}},
64
64
  )
65
65
  environment = tables.Column(attrs={"td": {"class": "table-td text-center"}})
66
- component = tables.Column(attrs={"td": {"class": "table-td text-center"}})
67
- component__group = tables.Column(attrs={"td": {"class": "table-td text-center"}})
66
+ incident_category = tables.Column(attrs={"td": {"class": "table-td text-center"}})
67
+ incident_category__group = tables.Column(attrs={"td": {"class": "table-td text-center"}})
68
68
 
69
69
 
70
- class ComponentsTable(tables.Table):
70
+ class IncidentCategoriesTable(tables.Table):
71
71
  class Meta:
72
- model = Component
72
+ model = IncidentCategory
73
73
  template_name = "incidents/table.html"
74
74
  fields = (
75
75
  "name",
@@ -93,12 +93,12 @@ class ComponentsTable(tables.Table):
93
93
  )
94
94
 
95
95
  @staticmethod
96
- def render_mtbf(record: Component, *args: Any) -> str:
96
+ def render_mtbf(record: IncidentCategory, *args: Any) -> str:
97
97
  mtbf: timedelta | None = record.mtbf # type: ignore[attr-defined]
98
98
  return str(mtbf).split(".", maxsplit=1)[0] if mtbf else "N/A"
99
99
 
100
100
  @staticmethod
101
- def render_incident_count(record: Component, *args: Any) -> int:
101
+ def render_incident_count(record: IncidentCategory, *args: Any) -> int:
102
102
  return int(record.incident_count) if record.incident_count else 0 # type: ignore[attr-defined]
103
103
 
104
104
  group__name = tables.Column(verbose_name="Group")
@@ -14,7 +14,7 @@
14
14
  {% include "./environment_pill.html" with environment=incident.environment only %}
15
15
  {% endif %}
16
16
  <span class="inline-flex font-semibold rounded-full px-2 text-xs leading-5 bg-neutral-300 text-neutral-600 ">
17
- {{ incident.component.name }} ({{ incident.component.group.name }})
17
+ {{ incident.incident_category.name }} ({{ incident.incident_category.group.name }})
18
18
  </span>
19
19
  {% include "./priority_pill.html" with priority=incident.priority only %}
20
20
  <p class="mt-2 text-neutral-500 dark:text-neutral-200 text-sm line-clamp-3">{{ incident.description|truncatechars:150 }}</p>
@@ -111,13 +111,13 @@
111
111
  Priority changed to: {{ incident_update.priority }}
112
112
  </p>
113
113
  {% endif %}
114
- {% if incident_update.component %}
114
+ {% if incident_update.incident_category %}
115
115
  <p class="mt-0.5">
116
116
  Component update
117
117
  </p>
118
118
  <p class="pl-2 mt-0.5 text-sm text-neutral-500 dark:text-neutral-300">
119
119
  Component impacted changed to:
120
- {{ incident_update.component }}
120
+ {{ incident_update.incident_category }}
121
121
  </p>
122
122
  {% endif %}
123
123
  {% if incident_update.communication_lead or incident_update.commander %}