firefighter-incident 0.0.12__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.
- firefighter/_version.py +16 -3
- firefighter/api/serializers.py +8 -8
- firefighter/api/urls.py +8 -1
- firefighter/api/views/_base.py +1 -1
- firefighter/api/views/components.py +5 -5
- firefighter/api/views/incidents.py +9 -9
- firefighter/firefighter/settings/components/raid.py +3 -0
- firefighter/incidents/admin.py +24 -24
- firefighter/incidents/factories.py +14 -5
- firefighter/incidents/forms/close_incident.py +4 -4
- firefighter/incidents/forms/create_incident.py +4 -4
- firefighter/incidents/forms/update_status.py +4 -4
- firefighter/incidents/menus.py +2 -2
- firefighter/incidents/migrations/0005_enable_from_p1_to_p5_priority.py +7 -5
- firefighter/incidents/migrations/0009_update_sla.py +7 -5
- firefighter/incidents/migrations/0019_set_security_components_private.py +67 -0
- firefighter/incidents/migrations/0020_create_incident_category_model.py +64 -0
- firefighter/incidents/migrations/0021_copy_component_data_to_incident_category.py +57 -0
- firefighter/incidents/migrations/0022_add_incident_category_fields.py +34 -0
- firefighter/incidents/migrations/0023_populate_incident_category_references.py +57 -0
- firefighter/incidents/migrations/0024_remove_component_fields_and_model.py +26 -0
- firefighter/incidents/migrations/0025_make_incident_category_required.py +24 -0
- firefighter/incidents/migrations/0026_alter_incidentcategory_options_and_more.py +39 -0
- firefighter/incidents/models/__init__.py +1 -1
- firefighter/incidents/models/group.py +1 -1
- firefighter/incidents/models/incident.py +15 -15
- firefighter/incidents/models/{component.py → incident_category.py} +30 -29
- firefighter/incidents/models/incident_update.py +3 -3
- firefighter/incidents/tables.py +9 -9
- firefighter/incidents/templates/layouts/partials/incident_card.html +1 -1
- firefighter/incidents/templates/layouts/partials/incident_timeline.html +2 -2
- firefighter/incidents/templates/pages/{component_detail.html → incident_category_detail.html} +13 -13
- firefighter/incidents/templates/pages/{component_list.html → incident_category_list.html} +2 -2
- firefighter/incidents/templates/pages/incident_detail.html +3 -3
- firefighter/incidents/urls.py +6 -6
- firefighter/incidents/views/components/details.py +9 -9
- firefighter/incidents/views/components/list.py +9 -9
- firefighter/incidents/views/reports.py +2 -2
- firefighter/incidents/views/users/details.py +2 -2
- firefighter/incidents/views/views.py +7 -7
- firefighter/jira_app/client.py +1 -1
- firefighter/logging/custom_json_formatter.py +2 -1
- firefighter/pagerduty/tasks/trigger_oncall.py +1 -1
- firefighter/raid/admin.py +0 -11
- firefighter/raid/client.py +3 -3
- firefighter/raid/forms.py +53 -19
- firefighter/raid/migrations/0003_delete_raidarea.py +16 -0
- firefighter/raid/models.py +2 -21
- firefighter/raid/serializers.py +5 -4
- firefighter/raid/service.py +29 -27
- firefighter/raid/signals/incident_created.py +4 -2
- firefighter/raid/utils.py +1 -1
- firefighter/raid/views/__init__.py +1 -1
- firefighter/raid/views/open_normal.py +2 -2
- firefighter/slack/admin.py +8 -8
- firefighter/slack/management/commands/switch_test_users.py +272 -0
- firefighter/slack/messages/slack_messages.py +5 -5
- firefighter/slack/migrations/0005_add_incident_categories_fields.py +33 -0
- firefighter/slack/migrations/0006_copy_components_to_incident_categories.py +57 -0
- firefighter/slack/migrations/0007_remove_components_fields.py +22 -0
- firefighter/slack/migrations/0008_alter_conversation_incident_categories_and_more.py +33 -0
- firefighter/slack/models/conversation.py +3 -3
- firefighter/slack/models/incident_channel.py +1 -1
- firefighter/slack/models/user.py +1 -1
- firefighter/slack/models/user_group.py +3 -3
- firefighter/slack/rules.py +1 -1
- firefighter/slack/signals/get_users.py +2 -2
- firefighter/slack/signals/incident_updated.py +1 -1
- firefighter/slack/utils.py +2 -2
- firefighter/slack/views/events/home.py +2 -2
- firefighter/slack/views/modals/base_modal/form_utils.py +15 -0
- firefighter/slack/views/modals/close.py +3 -3
- firefighter/slack/views/modals/open.py +25 -1
- firefighter/slack/views/modals/opening/check_current_incidents.py +2 -2
- firefighter/slack/views/modals/opening/details/critical.py +1 -1
- firefighter/slack/views/modals/opening/select_impact.py +5 -2
- firefighter/slack/views/modals/update_status.py +4 -4
- firefighter_fixtures/incidents/{components.json → incident_categories.json} +52 -52
- {firefighter_incident-0.0.12.dist-info → firefighter_incident-0.0.14.dist-info}/METADATA +2 -2
- {firefighter_incident-0.0.12.dist-info → firefighter_incident-0.0.14.dist-info}/RECORD +99 -77
- firefighter_tests/conftest.py +4 -5
- firefighter_tests/test_api/test_api_landbot.py +1 -1
- firefighter_tests/test_firefighter/test_sso.py +146 -0
- firefighter_tests/test_incidents/test_forms/test_form_utils.py +15 -15
- firefighter_tests/test_incidents/test_incident_urls.py +3 -3
- firefighter_tests/test_incidents/test_models/test_incident_category.py +165 -0
- firefighter_tests/test_incidents/test_models/test_incident_model.py +2 -2
- firefighter_tests/test_raid/test_priority_mapping.py +267 -0
- firefighter_tests/test_raid/test_raid_client.py +580 -0
- firefighter_tests/test_raid/test_raid_forms.py +795 -0
- firefighter_tests/test_raid/test_raid_models.py +185 -0
- firefighter_tests/test_raid/test_raid_serializers.py +507 -0
- firefighter_tests/test_raid/test_raid_service.py +442 -0
- firefighter_tests/test_raid/test_raid_views.py +196 -0
- firefighter_tests/test_slack/views/modals/test_close.py +6 -6
- firefighter_tests/test_slack/views/modals/test_update_status.py +4 -4
- firefighter_fixtures/raid/area.json +0 -1
- {firefighter_incident-0.0.12.dist-info → firefighter_incident-0.0.14.dist-info}/WHEEL +0 -0
- {firefighter_incident-0.0.12.dist-info → firefighter_incident-0.0.14.dist-info}/entry_points.txt +0 -0
- {firefighter_incident-0.0.12.dist-info → firefighter_incident-0.0.14.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Generated manually on 2025-08-19 - Step 1: Create IncidentCategory model
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
dependencies = [
|
|
11
|
+
("incidents", "0019_set_security_components_private"),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.CreateModel(
|
|
16
|
+
name="IncidentCategory",
|
|
17
|
+
fields=[
|
|
18
|
+
(
|
|
19
|
+
"id",
|
|
20
|
+
models.UUIDField(
|
|
21
|
+
default=uuid.uuid4,
|
|
22
|
+
editable=False,
|
|
23
|
+
primary_key=True,
|
|
24
|
+
serialize=False,
|
|
25
|
+
),
|
|
26
|
+
),
|
|
27
|
+
("name", models.CharField(max_length=128)),
|
|
28
|
+
("description", models.TextField(blank=True)),
|
|
29
|
+
(
|
|
30
|
+
"order",
|
|
31
|
+
models.IntegerField(
|
|
32
|
+
default=0,
|
|
33
|
+
help_text="Order of the incident category in the list. Should be unique per `Group`.",
|
|
34
|
+
),
|
|
35
|
+
),
|
|
36
|
+
(
|
|
37
|
+
"private",
|
|
38
|
+
models.BooleanField(
|
|
39
|
+
default=False,
|
|
40
|
+
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.",
|
|
41
|
+
),
|
|
42
|
+
),
|
|
43
|
+
(
|
|
44
|
+
"deploy_warning",
|
|
45
|
+
models.BooleanField(
|
|
46
|
+
default=True,
|
|
47
|
+
help_text="If true, a warning will be sent when creating an incident of high severity with this incident category.",
|
|
48
|
+
),
|
|
49
|
+
),
|
|
50
|
+
("created_at", models.DateTimeField(auto_now_add=True)),
|
|
51
|
+
("updated_at", models.DateTimeField(auto_now=True)),
|
|
52
|
+
(
|
|
53
|
+
"group",
|
|
54
|
+
models.ForeignKey(
|
|
55
|
+
on_delete=models.PROTECT,
|
|
56
|
+
to="incidents.group"
|
|
57
|
+
),
|
|
58
|
+
),
|
|
59
|
+
],
|
|
60
|
+
options={
|
|
61
|
+
"ordering": ["order"],
|
|
62
|
+
},
|
|
63
|
+
),
|
|
64
|
+
]
|
|
@@ -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.
|
|
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["
|
|
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
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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, "
|
|
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
|
-
|
|
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
|
|
632
|
-
"""Queryset for choices of
|
|
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
|
-
|
|
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="
|
|
666
|
+
queryset=Group.objects.all(), field_name="incident_category__group_id", label="Group"
|
|
667
667
|
)
|
|
668
|
-
|
|
669
|
-
queryset=
|
|
670
|
-
label="
|
|
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
|
|
44
|
-
model: type[
|
|
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[
|
|
50
|
+
queryset: QuerySet[IncidentCategory] | None = None,
|
|
51
51
|
metric_type: str = "time_to_fix",
|
|
52
52
|
field_name: str = "mtbf",
|
|
53
|
-
) -> QuerySet[
|
|
54
|
-
"""Returns a queryset of
|
|
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
|
-
|
|
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("
|
|
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[
|
|
99
|
-
) -> tuple[QuerySet[
|
|
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 =
|
|
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
|
|
139
|
-
objects:
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
178
|
+
return reverse("incidents:incident-category-detail", kwargs={"incident_category_id": self.id})
|
|
178
179
|
|
|
179
180
|
|
|
180
|
-
class
|
|
181
|
-
"""Set of filters for
|
|
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="
|
|
198
|
+
field_name="search", method="incident_category_search", label="Search"
|
|
198
199
|
)
|
|
199
200
|
|
|
200
201
|
@staticmethod
|
|
201
|
-
def
|
|
202
|
-
queryset: QuerySet[
|
|
203
|
-
) -> QuerySet[
|
|
204
|
-
"""Search
|
|
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[
|
|
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[
|
|
213
|
+
QuerySet[IncidentCategory]: Search results.
|
|
213
214
|
"""
|
|
214
|
-
return
|
|
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[
|
|
219
|
+
queryset: QuerySet[IncidentCategory],
|
|
219
220
|
_name: str,
|
|
220
221
|
value: tuple[datetime, datetime, Any, Any],
|
|
221
|
-
) -> QuerySet[
|
|
222
|
+
) -> QuerySet[IncidentCategory]:
|
|
222
223
|
gte, lte, _, _ = value
|
|
223
|
-
return
|
|
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
|
-
|
|
79
|
-
|
|
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
|
firefighter/incidents/tables.py
CHANGED
|
@@ -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
|
|
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
|
-
"
|
|
27
|
-
"
|
|
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
|
-
|
|
67
|
-
|
|
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
|
|
70
|
+
class IncidentCategoriesTable(tables.Table):
|
|
71
71
|
class Meta:
|
|
72
|
-
model =
|
|
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:
|
|
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:
|
|
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.
|
|
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>
|