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.
- firefighter/_version.py +16 -3
- firefighter/api/serializers.py +17 -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/confluence/signals/incident_updated.py +2 -2
- firefighter/firefighter/settings/components/raid.py +3 -0
- firefighter/incidents/admin.py +24 -24
- firefighter/incidents/enums.py +22 -2
- firefighter/incidents/factories.py +14 -5
- firefighter/incidents/forms/close_incident.py +4 -4
- firefighter/incidents/forms/closure_reason.py +45 -0
- firefighter/incidents/forms/create_incident.py +4 -4
- firefighter/incidents/forms/unified_incident.py +406 -0
- firefighter/incidents/forms/update_status.py +91 -5
- 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/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/migrations/0027_add_closure_fields.py +40 -0
- firefighter/incidents/migrations/0028_add_closure_reason_constraint.py +33 -0
- firefighter/incidents/migrations/0029_add_custom_fields_to_incident.py +22 -0
- firefighter/incidents/models/__init__.py +1 -1
- firefighter/incidents/models/group.py +1 -1
- firefighter/incidents/models/incident.py +47 -20
- firefighter/incidents/models/{component.py → incident_category.py} +30 -29
- firefighter/incidents/models/incident_update.py +3 -3
- firefighter/incidents/static/css/main.min.css +1 -1
- 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/layouts/partials/status_pill.html +1 -1
- 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 +5 -5
- 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/apps.py +9 -26
- firefighter/raid/client.py +5 -5
- firefighter/raid/forms.py +84 -213
- 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 +42 -15
- firefighter/raid/signals/incident_updated.py +3 -2
- firefighter/raid/utils.py +1 -1
- firefighter/raid/views/__init__.py +1 -1
- firefighter/slack/admin.py +8 -8
- firefighter/slack/management/commands/switch_test_users.py +272 -0
- firefighter/slack/messages/slack_messages.py +24 -9
- 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 +2 -2
- firefighter/slack/signals/create_incident_conversation.py +6 -0
- firefighter/slack/signals/get_users.py +2 -2
- firefighter/slack/signals/incident_updated.py +8 -2
- firefighter/slack/utils.py +2 -2
- firefighter/slack/views/events/home.py +2 -2
- firefighter/slack/views/modals/__init__.py +4 -0
- firefighter/slack/views/modals/base_modal/form_utils.py +78 -0
- firefighter/slack/views/modals/close.py +18 -5
- firefighter/slack/views/modals/closure_reason.py +193 -0
- firefighter/slack/views/modals/open.py +83 -12
- firefighter/slack/views/modals/opening/check_current_incidents.py +2 -2
- firefighter/slack/views/modals/opening/details/unified.py +203 -0
- firefighter/slack/views/modals/opening/select_impact.py +5 -2
- firefighter/slack/views/modals/opening/set_details.py +3 -2
- firefighter/slack/views/modals/postmortem.py +10 -2
- firefighter/slack/views/modals/update_status.py +32 -6
- firefighter/slack/views/modals/utils.py +51 -0
- firefighter_fixtures/incidents/{components.json → incident_categories.json} +52 -52
- {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/METADATA +2 -2
- {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/RECORD +133 -88
- 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_enums.py +100 -0
- firefighter_tests/test_incidents/test_forms/conftest.py +179 -0
- firefighter_tests/test_incidents/test_forms/test_closure_reason.py +91 -0
- firefighter_tests/test_incidents/test_forms/test_form_utils.py +15 -15
- firefighter_tests/test_incidents/test_forms/test_unified_incident_form.py +570 -0
- firefighter_tests/test_incidents/test_forms/test_unified_incident_form_integration.py +581 -0
- firefighter_tests/test_incidents/test_forms/test_unified_incident_form_p4_p5.py +410 -0
- firefighter_tests/test_incidents/test_forms/test_update_status_workflow.py +343 -0
- firefighter_tests/test_incidents/test_forms/test_workflow_transitions.py +167 -0
- 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 +70 -2
- firefighter_tests/test_raid/conftest.py +154 -0
- firefighter_tests/test_raid/test_p1_p3_jira_fields.py +372 -0
- 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 +552 -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_signals.py +187 -0
- firefighter_tests/test_raid/test_raid_views.py +196 -0
- firefighter_tests/test_slack/messages/__init__.py +0 -0
- firefighter_tests/test_slack/messages/test_slack_messages.py +367 -0
- firefighter_tests/test_slack/views/modals/conftest.py +140 -0
- firefighter_tests/test_slack/views/modals/test_close.py +71 -9
- firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +138 -0
- firefighter_tests/test_slack/views/modals/test_form_utils_multiple_choice.py +249 -0
- firefighter_tests/test_slack/views/modals/test_open.py +146 -2
- firefighter_tests/test_slack/views/modals/test_opening_unified.py +421 -0
- firefighter_tests/test_slack/views/modals/test_update_status.py +331 -7
- firefighter_tests/test_slack/views/modals/test_utils.py +135 -0
- firefighter/raid/views/open_normal.py +0 -139
- firefighter/slack/views/modals/opening/details/critical.py +0 -88
- firefighter_fixtures/raid/area.json +0 -1
- {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/WHEEL +0 -0
- {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/entry_points.txt +0 -0
- {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.
|
|
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["
|
|
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
|
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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, "
|
|
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
|
-
|
|
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.
|
|
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
|
|
632
|
-
"""Queryset for choices of
|
|
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
|
-
|
|
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="
|
|
693
|
+
queryset=Group.objects.all(), field_name="incident_category__group_id", label="Group"
|
|
667
694
|
)
|
|
668
|
-
|
|
669
|
-
queryset=
|
|
670
|
-
label="
|
|
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")
|