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
firefighter/_version.py
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
# file generated by setuptools-scm
|
|
2
2
|
# don't change, don't track in version control
|
|
3
3
|
|
|
4
|
-
__all__ = [
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
5
12
|
|
|
6
13
|
TYPE_CHECKING = False
|
|
7
14
|
if TYPE_CHECKING:
|
|
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
|
|
|
9
16
|
from typing import Union
|
|
10
17
|
|
|
11
18
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
12
20
|
else:
|
|
13
21
|
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
14
23
|
|
|
15
24
|
version: str
|
|
16
25
|
__version__: str
|
|
17
26
|
__version_tuple__: VERSION_TUPLE
|
|
18
27
|
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
19
30
|
|
|
20
|
-
__version__ = version = '0.0.
|
|
21
|
-
__version_tuple__ = version_tuple = (0, 0,
|
|
31
|
+
__version__ = version = '0.0.15'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 0, 15)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
firefighter/api/serializers.py
CHANGED
|
@@ -15,10 +15,10 @@ from rest_framework.fields import empty
|
|
|
15
15
|
from taggit.serializers import TaggitSerializer, TagListSerializerField
|
|
16
16
|
|
|
17
17
|
from firefighter.firefighter.utils import get_in
|
|
18
|
-
from firefighter.incidents.models.component import Component
|
|
19
18
|
from firefighter.incidents.models.environment import Environment
|
|
20
19
|
from firefighter.incidents.models.group import Group
|
|
21
20
|
from firefighter.incidents.models.incident import Incident
|
|
21
|
+
from firefighter.incidents.models.incident_category import IncidentCategory
|
|
22
22
|
from firefighter.incidents.models.incident_cost import IncidentCost
|
|
23
23
|
from firefighter.incidents.models.incident_cost_type import IncidentCostType
|
|
24
24
|
from firefighter.incidents.models.incident_membership import IncidentRole
|
|
@@ -157,11 +157,11 @@ class GroupSerializer(serializers.ModelSerializer[Group]):
|
|
|
157
157
|
fields = "__all__"
|
|
158
158
|
|
|
159
159
|
|
|
160
|
-
class
|
|
160
|
+
class IncidentCategorySerializer(serializers.ModelSerializer[IncidentCategory]):
|
|
161
161
|
group = GroupSerializer()
|
|
162
162
|
|
|
163
163
|
class Meta:
|
|
164
|
-
model =
|
|
164
|
+
model = IncidentCategory
|
|
165
165
|
fields = "__all__"
|
|
166
166
|
|
|
167
167
|
|
|
@@ -210,14 +210,15 @@ class IncidentSerializer(TaggitSerializer, serializers.ModelSerializer[Incident]
|
|
|
210
210
|
priority_id = serializers.PrimaryKeyRelatedField(
|
|
211
211
|
source="priority", queryset=Priority.objects.all(), write_only=True
|
|
212
212
|
)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
source="
|
|
213
|
+
incident_category = IncidentCategorySerializer(read_only=True)
|
|
214
|
+
incident_category_id = serializers.PrimaryKeyRelatedField(
|
|
215
|
+
source="incident_category", queryset=IncidentCategory.objects.all(), write_only=True
|
|
216
216
|
)
|
|
217
217
|
|
|
218
218
|
status = serializers.SerializerMethodField()
|
|
219
219
|
created_by = UserSerializer(read_only=True)
|
|
220
220
|
slack_channel_name = serializers.SerializerMethodField()
|
|
221
|
+
postmortem_url = serializers.SerializerMethodField()
|
|
221
222
|
|
|
222
223
|
created_by_email = CreatableSlugRelatedField[User](
|
|
223
224
|
source="created_by",
|
|
@@ -252,6 +253,13 @@ class IncidentSerializer(TaggitSerializer, serializers.ModelSerializer[Incident]
|
|
|
252
253
|
def get_slack_channel_name(obj: Incident) -> str:
|
|
253
254
|
return f"#{obj.slack_channel_name}" if obj.slack_channel_name else ""
|
|
254
255
|
|
|
256
|
+
@staticmethod
|
|
257
|
+
def get_postmortem_url(obj: Incident) -> str | None:
|
|
258
|
+
"""Return the Confluence post-mortem page URL if it exists."""
|
|
259
|
+
if hasattr(obj, "postmortem_for"):
|
|
260
|
+
return obj.postmortem_for.page_url
|
|
261
|
+
return None
|
|
262
|
+
|
|
255
263
|
def create(self, validated_data: dict[str, Any]) -> Incident:
|
|
256
264
|
return Incident.objects.declare(**validated_data)
|
|
257
265
|
|
|
@@ -274,14 +282,15 @@ class IncidentSerializer(TaggitSerializer, serializers.ModelSerializer[Incident]
|
|
|
274
282
|
"description",
|
|
275
283
|
"created_at",
|
|
276
284
|
"environment",
|
|
277
|
-
"
|
|
285
|
+
"incident_category",
|
|
278
286
|
"priority",
|
|
279
287
|
"status",
|
|
280
288
|
"slack_channel_name",
|
|
281
289
|
"status_page_url",
|
|
290
|
+
"postmortem_url",
|
|
282
291
|
"status",
|
|
283
292
|
"environment_id",
|
|
284
|
-
"
|
|
293
|
+
"incident_category_id",
|
|
285
294
|
"priority_id",
|
|
286
295
|
"created_by_email",
|
|
287
296
|
"tags",
|
firefighter/api/urls.py
CHANGED
|
@@ -33,11 +33,18 @@ router.register(
|
|
|
33
33
|
views.severities.PriorityViewSet,
|
|
34
34
|
basename="priorities",
|
|
35
35
|
)
|
|
36
|
+
# Legacy endpoint for backward compatibility
|
|
36
37
|
router.register(
|
|
37
38
|
r"components",
|
|
38
|
-
views.components.
|
|
39
|
+
views.components.IncidentCategoryViewSet,
|
|
39
40
|
basename="components",
|
|
40
41
|
)
|
|
42
|
+
# New preferred endpoint
|
|
43
|
+
router.register(
|
|
44
|
+
r"incident-categories",
|
|
45
|
+
views.components.IncidentCategoryViewSet,
|
|
46
|
+
basename="incident-categories",
|
|
47
|
+
)
|
|
41
48
|
router.register(
|
|
42
49
|
r"groups",
|
|
43
50
|
views.groups.GroupViewSet,
|
firefighter/api/views/_base.py
CHANGED
|
@@ -20,7 +20,7 @@ T_co = TypeVar("T_co", bound=Model, covariant=True)
|
|
|
20
20
|
examples=[
|
|
21
21
|
OpenApiExample(
|
|
22
22
|
name="Comma separated list of fields",
|
|
23
|
-
value="id,name,
|
|
23
|
+
value="id,name,incident_category.name,incident_category.group.name",
|
|
24
24
|
request_only=True,
|
|
25
25
|
),
|
|
26
26
|
OpenApiExample(
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from firefighter.api.serializers import
|
|
3
|
+
from firefighter.api.serializers import IncidentCategorySerializer
|
|
4
4
|
from firefighter.api.views._base import ReadOnlyModelViewSet
|
|
5
|
-
from firefighter.incidents.models.
|
|
5
|
+
from firefighter.incidents.models.incident_category import IncidentCategory
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
class
|
|
9
|
-
queryset =
|
|
10
|
-
serializer_class =
|
|
8
|
+
class IncidentCategoryViewSet(ReadOnlyModelViewSet[IncidentCategory]):
|
|
9
|
+
queryset = IncidentCategory.objects.all().select_related("group")
|
|
10
|
+
serializer_class = IncidentCategorySerializer
|
|
@@ -44,7 +44,7 @@ class ProcessAfterResponse(Response):
|
|
|
44
44
|
examples=[
|
|
45
45
|
OpenApiExample(
|
|
46
46
|
name="Comma separated list of fields",
|
|
47
|
-
value="id,name,
|
|
47
|
+
value="id,name,incident_category.name,incident_category.group.name",
|
|
48
48
|
request_only=True,
|
|
49
49
|
),
|
|
50
50
|
OpenApiExample(
|
|
@@ -73,8 +73,8 @@ class IncidentViewSet(
|
|
|
73
73
|
Incident.objects.all()
|
|
74
74
|
.select_related(
|
|
75
75
|
"priority",
|
|
76
|
-
"
|
|
77
|
-
"
|
|
76
|
+
"incident_category__group",
|
|
77
|
+
"incident_category",
|
|
78
78
|
"environment",
|
|
79
79
|
"conversation",
|
|
80
80
|
"created_by",
|
|
@@ -113,8 +113,8 @@ class IncidentViewSet(
|
|
|
113
113
|
"priority.name",
|
|
114
114
|
"title",
|
|
115
115
|
"description",
|
|
116
|
-
"
|
|
117
|
-
"
|
|
116
|
+
"incident_category.name",
|
|
117
|
+
"incident_category.group.name",
|
|
118
118
|
"created_at",
|
|
119
119
|
"slack_channel_name",
|
|
120
120
|
"status_page_url",
|
|
@@ -154,12 +154,12 @@ class IncidentViewSet(
|
|
|
154
154
|
OpenApiExample(
|
|
155
155
|
"Create an incident",
|
|
156
156
|
summary="Create an incident",
|
|
157
|
-
description="Create an incident, on INT, P4, with `Other`
|
|
157
|
+
description="Create an incident, on INT, P4, with `Other` incident category and John Doe as creator. All fields are required. The email must be a valid email of a ManoMano employee, that has a Slack account or has already used FireFighter before.",
|
|
158
158
|
value={
|
|
159
159
|
"title": "Title of the incident, limited to 128 characters.",
|
|
160
160
|
"description": "Longer description of the incident. No characters limit.",
|
|
161
161
|
"environment_id": "1b960430-995b-47e1-beab-23dbe3dbccbf",
|
|
162
|
-
"
|
|
162
|
+
"incident_category_id": "390a993a-d273-4db8-b7d6-190ab294961a",
|
|
163
163
|
"priority_id": "b814c9d2-48a8-4ac4-9c71-ff844e1b77f1",
|
|
164
164
|
"created_by_email": "john.doe@mycompany.com",
|
|
165
165
|
},
|
|
@@ -185,7 +185,7 @@ class IncidentViewSet(
|
|
|
185
185
|
"order": 3,
|
|
186
186
|
"default": False,
|
|
187
187
|
},
|
|
188
|
-
"
|
|
188
|
+
"incident_category": {
|
|
189
189
|
"id": "390a993a-d273-4db8-b7d6-190ab294961a",
|
|
190
190
|
"name": "Other",
|
|
191
191
|
"description": "",
|
|
@@ -241,7 +241,7 @@ class CreateIncidentViewSet(
|
|
|
241
241
|
viewsets.GenericViewSet[Incident],
|
|
242
242
|
):
|
|
243
243
|
queryset: QuerySet[Incident] = Incident.objects.all().select_related(
|
|
244
|
-
"priority", "
|
|
244
|
+
"priority", "incident_category__group", "incident_category", "environment", "conversation"
|
|
245
245
|
)
|
|
246
246
|
serializer_class = IncidentSerializer
|
|
247
247
|
filterset_class = IncidentFilterSet
|
|
@@ -39,7 +39,7 @@ def incident_updated_handler(
|
|
|
39
39
|
if (
|
|
40
40
|
"_status" in updated_fields
|
|
41
41
|
and incident_update.status
|
|
42
|
-
in {IncidentStatus.
|
|
42
|
+
in {IncidentStatus.MITIGATED, IncidentStatus.POST_MORTEM}
|
|
43
43
|
and incident.needs_postmortem
|
|
44
44
|
):
|
|
45
45
|
if not hasattr(incident, "postmortem_for"):
|
|
@@ -48,7 +48,7 @@ def incident_updated_handler(
|
|
|
48
48
|
elif (
|
|
49
49
|
"_status" in updated_fields
|
|
50
50
|
and incident_update.status
|
|
51
|
-
in {IncidentStatus.
|
|
51
|
+
in {IncidentStatus.MITIGATED, IncidentStatus.POST_MORTEM}
|
|
52
52
|
and not incident.needs_postmortem
|
|
53
53
|
):
|
|
54
54
|
publish_fixed_next_actions(incident)
|
|
@@ -20,3 +20,6 @@ if ENABLE_RAID:
|
|
|
20
20
|
|
|
21
21
|
RAID_TOOLBOX_URL: str = config("RAID_TOOLBOX_URL")
|
|
22
22
|
"Toolbox URL"
|
|
23
|
+
|
|
24
|
+
RAID_JIRA_INCIDENT_CATEGORY_FIELD: str = config("RAID_JIRA_INCIDENT_CATEGORY_FIELD", default="")
|
|
25
|
+
"Jira custom field ID for incident category (e.g. 'customfield_12345')"
|
firefighter/incidents/admin.py
CHANGED
|
@@ -19,10 +19,10 @@ from django.utils.translation import ngettext
|
|
|
19
19
|
from slack_sdk.errors import SlackApiError
|
|
20
20
|
|
|
21
21
|
from firefighter.incidents.models import (
|
|
22
|
-
Component,
|
|
23
22
|
Environment,
|
|
24
23
|
Group,
|
|
25
24
|
Incident,
|
|
25
|
+
IncidentCategory,
|
|
26
26
|
IncidentUpdate,
|
|
27
27
|
Severity,
|
|
28
28
|
User,
|
|
@@ -63,7 +63,7 @@ logger = logging.getLogger(__name__)
|
|
|
63
63
|
|
|
64
64
|
# Append inlines to these objects, from other modules
|
|
65
65
|
user_inlines: list[type[InlineModelAdmin[Any, User]]] = []
|
|
66
|
-
|
|
66
|
+
incident_category_inlines: list[type[InlineModelAdmin[Any, IncidentCategory]]] = []
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
class IncidentCostInline(admin.TabularInline[IncidentCost, Incident]):
|
|
@@ -97,9 +97,9 @@ class IncidentMembershipInline(admin.StackedInline[IncidentMembership, Incident]
|
|
|
97
97
|
incident_inlines: MutableSequence[type[InlineModelAdmin[Any, Incident]]] = []
|
|
98
98
|
|
|
99
99
|
|
|
100
|
-
@admin.register(
|
|
101
|
-
class
|
|
102
|
-
model =
|
|
100
|
+
@admin.register(IncidentCategory)
|
|
101
|
+
class IncidentCategoryAdmin(admin.ModelAdmin[IncidentCategory]):
|
|
102
|
+
model = IncidentCategory
|
|
103
103
|
list_display = ["name", "group", "order", "private", "deploy_warning"]
|
|
104
104
|
list_editable = ["order", "group", "private"]
|
|
105
105
|
list_display_links = ["name"]
|
|
@@ -111,7 +111,7 @@ class ComponentAdmin(admin.ModelAdmin[Component]):
|
|
|
111
111
|
"created_at",
|
|
112
112
|
"updated_at",
|
|
113
113
|
]
|
|
114
|
-
inlines =
|
|
114
|
+
inlines = incident_category_inlines
|
|
115
115
|
|
|
116
116
|
fieldsets = (
|
|
117
117
|
(
|
|
@@ -132,8 +132,8 @@ class ComponentAdmin(admin.ModelAdmin[Component]):
|
|
|
132
132
|
)
|
|
133
133
|
|
|
134
134
|
|
|
135
|
-
class
|
|
136
|
-
model =
|
|
135
|
+
class IncidentCategoryInline(admin.StackedInline[IncidentCategory, IncidentCategory]):
|
|
136
|
+
model = IncidentCategory
|
|
137
137
|
fields = ["name", "order"]
|
|
138
138
|
|
|
139
139
|
|
|
@@ -218,7 +218,7 @@ class IncidentUpdateInline(admin.StackedInline[IncidentUpdate, Incident]):
|
|
|
218
218
|
"_status",
|
|
219
219
|
"priority",
|
|
220
220
|
"message",
|
|
221
|
-
"
|
|
221
|
+
"incident_category",
|
|
222
222
|
"created_by",
|
|
223
223
|
]
|
|
224
224
|
extra = 0
|
|
@@ -226,7 +226,7 @@ class IncidentUpdateInline(admin.StackedInline[IncidentUpdate, Incident]):
|
|
|
226
226
|
"priority",
|
|
227
227
|
"message",
|
|
228
228
|
"_status",
|
|
229
|
-
"
|
|
229
|
+
"incident_category",
|
|
230
230
|
"created_by",
|
|
231
231
|
"commander",
|
|
232
232
|
"communication_lead",
|
|
@@ -239,11 +239,11 @@ class IncidentUpdateInline(admin.StackedInline[IncidentUpdate, Incident]):
|
|
|
239
239
|
.get_queryset(request)
|
|
240
240
|
.select_related(
|
|
241
241
|
"priority",
|
|
242
|
-
"
|
|
242
|
+
"incident_category",
|
|
243
243
|
"incident",
|
|
244
244
|
"incident__priority",
|
|
245
|
-
"
|
|
246
|
-
"
|
|
245
|
+
"incident__incident_category",
|
|
246
|
+
"incident_category__group",
|
|
247
247
|
"created_by",
|
|
248
248
|
"commander",
|
|
249
249
|
"communication_lead",
|
|
@@ -398,7 +398,7 @@ class IncidentAdmin(admin.ModelAdmin[Incident]):
|
|
|
398
398
|
# Return None to display the change list page again.
|
|
399
399
|
|
|
400
400
|
date_hierarchy = "created_at"
|
|
401
|
-
autocomplete_fields = ["created_by", "
|
|
401
|
+
autocomplete_fields = ["created_by", "incident_category"]
|
|
402
402
|
actions: list[_ActionCallable[Any, Incident]] = [
|
|
403
403
|
compute_metrics,
|
|
404
404
|
compute_and_purge_metrics,
|
|
@@ -410,14 +410,14 @@ class IncidentAdmin(admin.ModelAdmin[Incident]):
|
|
|
410
410
|
"short_description",
|
|
411
411
|
"_status",
|
|
412
412
|
"priority",
|
|
413
|
-
"
|
|
413
|
+
"incident_category",
|
|
414
414
|
"environment",
|
|
415
415
|
"created_at",
|
|
416
416
|
"updated_at",
|
|
417
417
|
]
|
|
418
418
|
|
|
419
419
|
list_display_links = ["id", "title"]
|
|
420
|
-
list_filter = ("_status", "priority", "
|
|
420
|
+
list_filter = ("_status", "priority", "incident_category", "environment")
|
|
421
421
|
readonly_fields = (
|
|
422
422
|
"created_at",
|
|
423
423
|
"updated_at",
|
|
@@ -429,7 +429,7 @@ class IncidentAdmin(admin.ModelAdmin[Incident]):
|
|
|
429
429
|
|
|
430
430
|
list_select_related = (
|
|
431
431
|
"priority",
|
|
432
|
-
"
|
|
432
|
+
"incident_category__group",
|
|
433
433
|
"environment",
|
|
434
434
|
)
|
|
435
435
|
list_max_show_all = 1000
|
|
@@ -450,7 +450,7 @@ class IncidentAdmin(admin.ModelAdmin[Incident]):
|
|
|
450
450
|
)
|
|
451
451
|
},
|
|
452
452
|
),
|
|
453
|
-
(_("Relations"), {"fields": ("priority", "
|
|
453
|
+
(_("Relations"), {"fields": ("priority", "incident_category", "environment")}),
|
|
454
454
|
(
|
|
455
455
|
_("User Roles"),
|
|
456
456
|
{
|
|
@@ -506,10 +506,10 @@ class IncidentAdmin(admin.ModelAdmin[Incident]):
|
|
|
506
506
|
def _get_select_related(self) -> list[str]:
|
|
507
507
|
select_related = [
|
|
508
508
|
"priority",
|
|
509
|
-
"
|
|
509
|
+
"incident_category__group",
|
|
510
510
|
"environment",
|
|
511
511
|
"conversation",
|
|
512
|
-
"
|
|
512
|
+
"incident_category",
|
|
513
513
|
"created_by",
|
|
514
514
|
]
|
|
515
515
|
if apps.apps.is_installed("firefighter.confluence"):
|
|
@@ -525,13 +525,13 @@ class IncidentUpdateAdmin(admin.ModelAdmin[IncidentUpdate]):
|
|
|
525
525
|
"_status",
|
|
526
526
|
"priority",
|
|
527
527
|
"message",
|
|
528
|
-
"
|
|
528
|
+
"incident_category",
|
|
529
529
|
"created_by",
|
|
530
530
|
]
|
|
531
531
|
list_display_links = ["_status"]
|
|
532
|
-
list_filter = ("_status", "priority", "
|
|
532
|
+
list_filter = ("_status", "priority", "incident_category", "event_type")
|
|
533
533
|
readonly_fields = ("created_at", "updated_at", "incident")
|
|
534
|
-
list_select_related = ("priority", "
|
|
534
|
+
list_select_related = ("priority", "incident_category", "incident", "created_by")
|
|
535
535
|
search_fields = ["description", "message"]
|
|
536
536
|
|
|
537
537
|
|
|
@@ -544,7 +544,7 @@ class GroupAdmin(admin.ModelAdmin[Group]):
|
|
|
544
544
|
]
|
|
545
545
|
ordering = ["order"]
|
|
546
546
|
list_display_links = ["name"]
|
|
547
|
-
inlines = [
|
|
547
|
+
inlines = [IncidentCategoryInline]
|
|
548
548
|
search_fields = ["name"]
|
|
549
549
|
|
|
550
550
|
|
firefighter/incidents/enums.py
CHANGED
|
@@ -6,8 +6,8 @@ from django.db import models
|
|
|
6
6
|
class IncidentStatus(models.IntegerChoices):
|
|
7
7
|
OPEN = 10, "Open"
|
|
8
8
|
INVESTIGATING = 20, "Investigating"
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
MITIGATING = 30, "Mitigating"
|
|
10
|
+
MITIGATED = 40, "Mitigated"
|
|
11
11
|
POST_MORTEM = 50, "Post-mortem"
|
|
12
12
|
CLOSED = 60, "Closed"
|
|
13
13
|
|
|
@@ -30,3 +30,23 @@ class IncidentStatus(models.IntegerChoices):
|
|
|
30
30
|
@staticmethod
|
|
31
31
|
def choices_lt(val: int) -> list[tuple[int, str]]:
|
|
32
32
|
return [i for i in IncidentStatus.choices if i[0] < val]
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def choices_lte(val: int) -> list[tuple[int, str]]:
|
|
36
|
+
return [i for i in IncidentStatus.choices if i[0] <= val]
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def choices_lte_skip_postmortem(val: int) -> list[tuple[int, str]]:
|
|
40
|
+
"""Return choices up to val but excluding POST_MORTEM (for P3+ incidents)."""
|
|
41
|
+
return [i for i in IncidentStatus.choices if i[0] <= val and i[0] != IncidentStatus.POST_MORTEM]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ClosureReason(models.TextChoices):
|
|
45
|
+
"""Reasons for direct incident closure bypassing normal workflow."""
|
|
46
|
+
|
|
47
|
+
RESOLVED = "resolved", "Resolved normally"
|
|
48
|
+
DUPLICATE = "duplicate", "Duplicate incident"
|
|
49
|
+
FALSE_POSITIVE = "false_positive", "False alarm - no actual issue"
|
|
50
|
+
SUPERSEDED = "superseded", "Superseded by another incident"
|
|
51
|
+
EXTERNAL = "external", "External dependency/known issue"
|
|
52
|
+
CANCELLED = "cancelled", "Cancelled - no longer relevant"
|
|
@@ -11,10 +11,10 @@ from factory.fuzzy import FuzzyChoice, FuzzyDateTime, FuzzyInteger
|
|
|
11
11
|
|
|
12
12
|
from firefighter.incidents.enums import IncidentStatus
|
|
13
13
|
from firefighter.incidents.models import (
|
|
14
|
-
Component,
|
|
15
14
|
Environment,
|
|
16
15
|
Group,
|
|
17
16
|
Incident,
|
|
17
|
+
IncidentCategory,
|
|
18
18
|
Priority,
|
|
19
19
|
User,
|
|
20
20
|
)
|
|
@@ -38,7 +38,12 @@ class GroupFactory(DjangoModelFactory[Group]):
|
|
|
38
38
|
order = FuzzyInteger(100, 1000) # type: ignore[no-untyped-call]
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
class
|
|
41
|
+
class PriorityFactory(DjangoModelFactory[Priority]):
|
|
42
|
+
"""Factory for creating Priority instances in tests.
|
|
43
|
+
|
|
44
|
+
Creates Priority objects with random values. Use specific values in tests
|
|
45
|
+
when testing priority-specific behavior (e.g., P1-P5 JIRA mapping).
|
|
46
|
+
"""
|
|
42
47
|
class Meta:
|
|
43
48
|
model = Priority
|
|
44
49
|
|
|
@@ -47,6 +52,10 @@ class SeverityFactory(DjangoModelFactory[Priority]):
|
|
|
47
52
|
order = FuzzyInteger(100, 1000) # type: ignore[no-untyped-call]
|
|
48
53
|
|
|
49
54
|
|
|
55
|
+
# Legacy alias - will be removed when Severity model is removed
|
|
56
|
+
SeverityFactory = PriorityFactory
|
|
57
|
+
|
|
58
|
+
|
|
50
59
|
class EnvironmentFactory(DjangoModelFactory[Environment]):
|
|
51
60
|
class Meta:
|
|
52
61
|
model = Environment
|
|
@@ -56,9 +65,9 @@ class EnvironmentFactory(DjangoModelFactory[Environment]):
|
|
|
56
65
|
order = FuzzyInteger(100, 1000) # type: ignore[no-untyped-call]
|
|
57
66
|
|
|
58
67
|
|
|
59
|
-
class
|
|
68
|
+
class IncidentCategoryFactory(DjangoModelFactory[IncidentCategory]):
|
|
60
69
|
class Meta:
|
|
61
|
-
model =
|
|
70
|
+
model = IncidentCategory
|
|
62
71
|
|
|
63
72
|
name = Faker("text", max_nb_chars=30) # type: ignore[no-untyped-call]
|
|
64
73
|
description = Faker("text", max_nb_chars=50) # type: ignore[no-untyped-call]
|
|
@@ -86,7 +95,7 @@ class IncidentFactory(DjangoModelFactory[Incident]):
|
|
|
86
95
|
tzinfo=timezone.get_current_timezone(),
|
|
87
96
|
)
|
|
88
97
|
) # type: ignore[no-untyped-call]
|
|
89
|
-
|
|
98
|
+
incident_category = Iterator(IncidentCategory.objects.all()) # type: ignore[no-untyped-call]
|
|
90
99
|
priority = Iterator(Priority.objects.all()) # type: ignore[no-untyped-call]
|
|
91
100
|
environment = Iterator(Environment.objects.all()) # type: ignore[no-untyped-call]
|
|
92
101
|
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from django import forms
|
|
4
4
|
|
|
5
5
|
from firefighter.incidents.forms.utils import GroupedModelChoiceField
|
|
6
|
-
from firefighter.incidents.models import
|
|
6
|
+
from firefighter.incidents.models import IncidentCategory
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class CloseIncidentForm(forms.Form):
|
|
@@ -26,10 +26,10 @@ class CloseIncidentForm(forms.Form):
|
|
|
26
26
|
max_length=1200,
|
|
27
27
|
required=False,
|
|
28
28
|
)
|
|
29
|
-
|
|
29
|
+
incident_category = GroupedModelChoiceField(
|
|
30
30
|
choices_groupby="group",
|
|
31
|
-
label="
|
|
32
|
-
queryset=
|
|
31
|
+
label="Incident category",
|
|
32
|
+
queryset=IncidentCategory.objects.all()
|
|
33
33
|
.select_related("group")
|
|
34
34
|
.order_by(
|
|
35
35
|
"group__order",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Form for incident closure with reason when closing from early statuses."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from django import forms
|
|
7
|
+
|
|
8
|
+
from firefighter.incidents.enums import ClosureReason
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from firefighter.incidents.models import Incident
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class IncidentClosureReasonForm(forms.Form):
|
|
15
|
+
"""Form for closing an incident with a mandatory reason from early statuses."""
|
|
16
|
+
|
|
17
|
+
closure_reason = forms.ChoiceField(
|
|
18
|
+
label="Closure Reason",
|
|
19
|
+
choices=ClosureReason.choices,
|
|
20
|
+
required=True,
|
|
21
|
+
help_text="Select the reason for closing this incident",
|
|
22
|
+
)
|
|
23
|
+
closure_reference = forms.CharField(
|
|
24
|
+
label="Reference (optional)",
|
|
25
|
+
max_length=100,
|
|
26
|
+
required=False,
|
|
27
|
+
help_text="Reference incident ID or external link for context (e.g., #1234 or URL)",
|
|
28
|
+
)
|
|
29
|
+
message = forms.CharField(
|
|
30
|
+
label="Closure Message",
|
|
31
|
+
widget=forms.Textarea,
|
|
32
|
+
required=True,
|
|
33
|
+
help_text="Brief explanation of why this incident is being closed",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def __init__(self, *args: Any, incident: Incident | None = None, **kwargs: Any) -> None: # noqa: ARG002
|
|
37
|
+
super().__init__(*args, **kwargs)
|
|
38
|
+
|
|
39
|
+
# Exclude RESOLVED from choices as it's for normal workflow closure
|
|
40
|
+
closure_field = self.fields["closure_reason"]
|
|
41
|
+
if hasattr(closure_field, "choices"):
|
|
42
|
+
closure_field.choices = [
|
|
43
|
+
choice for choice in ClosureReason.choices
|
|
44
|
+
if choice[0] != ClosureReason.RESOLVED
|
|
45
|
+
]
|
|
@@ -6,7 +6,7 @@ from django import forms
|
|
|
6
6
|
|
|
7
7
|
from firefighter.incidents.forms.select_impact import SelectImpactForm
|
|
8
8
|
from firefighter.incidents.forms.utils import GroupedModelChoiceField
|
|
9
|
-
from firefighter.incidents.models import
|
|
9
|
+
from firefighter.incidents.models import Environment, IncidentCategory, Priority
|
|
10
10
|
from firefighter.incidents.models.incident import Incident
|
|
11
11
|
from firefighter.incidents.signals import create_incident_conversation
|
|
12
12
|
|
|
@@ -52,11 +52,11 @@ class CreateIncidentForm(CreateIncidentFormBase):
|
|
|
52
52
|
max_length=1200,
|
|
53
53
|
)
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
incident_category = GroupedModelChoiceField(
|
|
56
56
|
choices_groupby="group",
|
|
57
|
-
label="
|
|
57
|
+
label="Incident category",
|
|
58
58
|
queryset=(
|
|
59
|
-
|
|
59
|
+
IncidentCategory.objects.all()
|
|
60
60
|
.select_related("group")
|
|
61
61
|
.order_by(
|
|
62
62
|
"group__order",
|