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
|
@@ -111,13 +111,13 @@
|
|
|
111
111
|
Priority changed to: {{ incident_update.priority }}
|
|
112
112
|
</p>
|
|
113
113
|
{% endif %}
|
|
114
|
-
{% if incident_update.
|
|
114
|
+
{% if incident_update.incident_category %}
|
|
115
115
|
<p class="mt-0.5">
|
|
116
116
|
Component update
|
|
117
117
|
</p>
|
|
118
118
|
<p class="pl-2 mt-0.5 text-sm text-neutral-500 dark:text-neutral-300">
|
|
119
119
|
Component impacted changed to:
|
|
120
|
-
{{ incident_update.
|
|
120
|
+
{{ incident_update.incident_category }}
|
|
121
121
|
</p>
|
|
122
122
|
{% endif %}
|
|
123
123
|
{% if incident_update.communication_lead or incident_update.commander %}
|
firefighter/incidents/templates/pages/{component_detail.html → incident_category_detail.html}
RENAMED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
<div class="flex items-center space-x-5">
|
|
8
8
|
<div>
|
|
9
9
|
<h1 class="text-xl leading-6 font-bold text-neutral-900 dark:text-neutral-50">
|
|
10
|
-
{{
|
|
10
|
+
{{ incident_category.name }}
|
|
11
11
|
</h1>
|
|
12
12
|
</div>
|
|
13
13
|
</div>
|
|
14
14
|
<div class="mt-6 flex flex-col-reverse justify-stretch space-y-4 space-y-reverse sm:flex-row-reverse sm:justify-end sm:space-x-reverse sm:space-y-0 sm:space-x-3 md:mt-0 md:flex-row md:space-x-3">
|
|
15
15
|
|
|
16
|
-
{% if perms.incidents.
|
|
16
|
+
{% if perms.incidents.change_incident_categories %}
|
|
17
17
|
<a href="{{ admin_edit_url }}" target="_blank" rel="noopener noreferrer" class="btn btn-primary gap-2 ">
|
|
18
18
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
19
19
|
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z" />
|
|
@@ -27,26 +27,26 @@
|
|
|
27
27
|
|
|
28
28
|
<div class="mt-8 max-w-3xl mx-auto grid grid-cols-1 gap-6 sm:px-6 lg:max-w-7xl lg:grid-flow-col-dense lg:grid-cols-12">
|
|
29
29
|
<div class="space-y-6 lg:col-start-1 lg:col-span-7">
|
|
30
|
-
{% component "card" card_title="
|
|
30
|
+
{% component "card" card_title="Incident category information" id="incident-category-information" %}
|
|
31
31
|
{% fill "card_content" %}
|
|
32
32
|
<dl class="grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2">
|
|
33
|
-
{% if
|
|
33
|
+
{% if incident_category.description %}
|
|
34
34
|
<div class="sm:col-span-2">
|
|
35
35
|
<dt class="text-sm font-medium text-neutral-500 dark:text-neutral-300">Description</dt>
|
|
36
|
-
<dd class="mt-1 text-sm text-neutral-900 dark:text-neutral-100"> {{
|
|
36
|
+
<dd class="mt-1 text-sm text-neutral-900 dark:text-neutral-100"> {{ incident_category.description|urlize|linebreaksbr }}</dd>
|
|
37
37
|
</div>
|
|
38
38
|
{% endif %}
|
|
39
39
|
<div class="sm:col-span-1">
|
|
40
40
|
<dt class="text-sm font-medium text-neutral-500 dark:text-neutral-300">Group</dt>
|
|
41
|
-
<dd class="mt-1 text-sm text-neutral-900 dark:text-neutral-100"> {{
|
|
41
|
+
<dd class="mt-1 text-sm text-neutral-900 dark:text-neutral-100"> {{ incident_category.group.name }}</dd>
|
|
42
42
|
</div>
|
|
43
43
|
</dl>
|
|
44
44
|
{% endfill %}
|
|
45
45
|
{% endcomponent %}
|
|
46
|
-
{% component "card" card_title="Responders groups" card_subtitle="People from these groups will be added to incidents created with this
|
|
46
|
+
{% component "card" card_title="Responders groups" card_subtitle="People from these groups will be added to incidents created with this incident category" id="incident-responders-groups" %}
|
|
47
47
|
{% fill "card_content" %}
|
|
48
48
|
<ul role="list" class="mt-3 grid grid-cols-1 gap-5 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3">
|
|
49
|
-
{% for usergroup in
|
|
49
|
+
{% for usergroup in incident_category.usergroups.all %}
|
|
50
50
|
<li class="col-span-1 flex rounded-md shadow-sm">
|
|
51
51
|
<div class="flex flex-1 items-center justify-between truncate rounded-md border border-neutral-200 dark:border-neutral-700">
|
|
52
52
|
<div class="flex-1 truncate px-4 py-2 text-sm">
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
</div>
|
|
57
57
|
</li>
|
|
58
58
|
{% endfor %}
|
|
59
|
-
{% for conversation in
|
|
59
|
+
{% for conversation in incident_category.conversations.all %}
|
|
60
60
|
<li class="col-span-1 flex rounded-md shadow-sm">
|
|
61
61
|
<div class="flex flex-1 items-center justify-between truncate rounded-md border border-neutral-200 dark:border-neutral-700">
|
|
62
62
|
<div class="flex-1 truncate px-4 py-2 text-sm">
|
|
@@ -74,13 +74,13 @@
|
|
|
74
74
|
<div class="lg:col-start-8 lg:col-span-5 space-y-6 ">
|
|
75
75
|
{% component "card" card_title="Other components in the same group" id="incident-timeline" %}
|
|
76
76
|
{% fill "card_title" %}
|
|
77
|
-
<h2 id="{{ id }}-title" class="text-lg leading-6 font-medium text-neutral-900 dark:text-neutral-50">Other
|
|
77
|
+
<h2 id="{{ id }}-title" class="text-lg leading-6 font-medium text-neutral-900 dark:text-neutral-50">Other incident categories in "{{ incident_category.group.name }}"</h2>
|
|
78
78
|
{% endfill %}
|
|
79
79
|
{% fill "card_content" %}
|
|
80
80
|
<ul class="mx-6 list-disc">
|
|
81
|
-
{% for
|
|
82
|
-
{% if
|
|
83
|
-
<li><a href="{{
|
|
81
|
+
{% for other_incident_category in incident_category.group.incidentcategory_set.all %}
|
|
82
|
+
{% if other_incident_category.id != incident_category.id %}
|
|
83
|
+
<li><a href="{{ other_incident_category.get_absolute_url }}" class="link"> {{ other_incident_category.name }}</a></li>
|
|
84
84
|
{% endif %}
|
|
85
85
|
{% endfor %}
|
|
86
86
|
</ul>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{% extends '../layouts/view_filters.html' %}
|
|
2
2
|
|
|
3
3
|
{% block page_title %}
|
|
4
|
-
|
|
4
|
+
Incident category list <div role="status" class="hx-progress htmx-indicator inline">
|
|
5
5
|
<svg class="inline mr-2 w-6 h-6 text-gray-200 animate-spin dark:text-gray-600 fill-primary" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
6
6
|
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
|
|
7
7
|
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
{% block page_actions %}
|
|
14
14
|
{{ block.super }}
|
|
15
|
-
{% component "export_button" base_url="api:
|
|
15
|
+
{% component "export_button" base_url="api:incident-categories-list" %}{% endcomponent %}
|
|
16
16
|
{% endblock page_actions %}
|
|
17
17
|
|
|
18
18
|
{% block page_content %}
|
|
@@ -52,16 +52,16 @@
|
|
|
52
52
|
<dd class="mt-1 text-sm text-neutral-900 dark:text-neutral-100"> {% include "../layouts/partials/environment_pill.html" with environment=incident.environment only %}
|
|
53
53
|
</dd>
|
|
54
54
|
</div>
|
|
55
|
-
{% if incident.
|
|
55
|
+
{% if incident.incident_category is not none %}
|
|
56
56
|
<div class="sm:col-span-1">
|
|
57
57
|
<dt class="text-sm font-medium text-neutral-500 dark:text-neutral-300">Group</dt>
|
|
58
|
-
<dd class="mt-1 text-sm text-neutral-900 dark:text-neutral-100"> {{ incident.
|
|
58
|
+
<dd class="mt-1 text-sm text-neutral-900 dark:text-neutral-100"> {{ incident.incident_category.group }}</dd>
|
|
59
59
|
</div>
|
|
60
60
|
{% endif %}
|
|
61
61
|
|
|
62
62
|
<div class="sm:col-span-1">
|
|
63
63
|
<dt class="text-sm font-medium text-neutral-500 dark:text-neutral-300">Component</dt>
|
|
64
|
-
<dd class="mt-1 text-sm text-neutral-900 dark:text-neutral-100"><a class="link" href="{{ incident.
|
|
64
|
+
<dd class="mt-1 text-sm text-neutral-900 dark:text-neutral-100"><a class="link" href="{{ incident.incident_category.get_absolute_url }}">{{ incident.incident_category }}</a></dd>
|
|
65
65
|
</div>
|
|
66
66
|
|
|
67
67
|
|
firefighter/incidents/urls.py
CHANGED
|
@@ -3,8 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
from django.urls import path
|
|
4
4
|
|
|
5
5
|
from firefighter.incidents.views import views
|
|
6
|
-
from firefighter.incidents.views.components.details import
|
|
7
|
-
from firefighter.incidents.views.components.list import
|
|
6
|
+
from firefighter.incidents.views.components.details import IncidentCategoryDetailView
|
|
7
|
+
from firefighter.incidents.views.components.list import IncidentCategoriesViewList
|
|
8
8
|
from firefighter.incidents.views.docs.metrics import MetricsView
|
|
9
9
|
from firefighter.incidents.views.docs.role_types import (
|
|
10
10
|
RoleTypeDetailView,
|
|
@@ -37,11 +37,11 @@ urlpatterns = [
|
|
|
37
37
|
views.IncidentStatisticsView.as_view(),
|
|
38
38
|
name="incident-statistics",
|
|
39
39
|
),
|
|
40
|
-
path("
|
|
40
|
+
path("incident-category/", IncidentCategoriesViewList.as_view(), name="incident-category-list"),
|
|
41
41
|
path(
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
name="
|
|
42
|
+
"incident-category/<uuid:incident_category_id>/",
|
|
43
|
+
IncidentCategoryDetailView.as_view(),
|
|
44
|
+
name="incident-category-detail",
|
|
45
45
|
),
|
|
46
46
|
path(
|
|
47
47
|
"user/<uuid:user_id>/",
|
|
@@ -6,28 +6,28 @@ from typing import Any
|
|
|
6
6
|
from django.db.models.query import Prefetch
|
|
7
7
|
|
|
8
8
|
from firefighter.firefighter.views import CustomDetailView
|
|
9
|
-
from firefighter.incidents.models import
|
|
9
|
+
from firefighter.incidents.models import IncidentCategory
|
|
10
10
|
|
|
11
11
|
logger = logging.getLogger(__name__)
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
class
|
|
15
|
-
template_name = "pages/
|
|
16
|
-
context_object_name = "
|
|
17
|
-
pk_url_kwarg = "
|
|
18
|
-
model =
|
|
14
|
+
class IncidentCategoryDetailView(CustomDetailView[IncidentCategory]):
|
|
15
|
+
template_name = "pages/incident_category_detail.html"
|
|
16
|
+
context_object_name = "incident_category"
|
|
17
|
+
pk_url_kwarg = "incident_category_id"
|
|
18
|
+
model = IncidentCategory
|
|
19
19
|
select_related = ["group"]
|
|
20
20
|
|
|
21
|
-
queryset =
|
|
21
|
+
queryset = IncidentCategory.objects.select_related(*select_related).prefetch_related(
|
|
22
22
|
Prefetch("usergroups")
|
|
23
23
|
)
|
|
24
24
|
|
|
25
25
|
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
|
26
26
|
context = super().get_context_data(**kwargs)
|
|
27
|
-
|
|
27
|
+
incident_category: IncidentCategory = context["incident_category"]
|
|
28
28
|
|
|
29
29
|
additional_context = {
|
|
30
|
-
"page_title": f"{
|
|
30
|
+
"page_title": f"{incident_category.name} Incident Category",
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
return {**context, **additional_context}
|
|
@@ -7,9 +7,9 @@ from django.utils import timezone
|
|
|
7
7
|
from django_filters.views import FilterView
|
|
8
8
|
from django_tables2.views import SingleTableMixin
|
|
9
9
|
|
|
10
|
-
from firefighter.incidents.models import
|
|
11
|
-
from firefighter.incidents.models.
|
|
12
|
-
from firefighter.incidents.tables import
|
|
10
|
+
from firefighter.incidents.models import IncidentCategory
|
|
11
|
+
from firefighter.incidents.models.incident_category import IncidentCategoryFilterSet
|
|
12
|
+
from firefighter.incidents.tables import IncidentCategoriesTable
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
15
|
from django_tables2.tables import Table
|
|
@@ -21,11 +21,11 @@ logger = logging.getLogger(__name__)
|
|
|
21
21
|
TZ = timezone.get_current_timezone()
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
class
|
|
25
|
-
table_class =
|
|
26
|
-
context_object_name = "
|
|
27
|
-
filterset_class =
|
|
28
|
-
model =
|
|
24
|
+
class IncidentCategoriesViewList(SingleTableMixin, FilterView):
|
|
25
|
+
table_class = IncidentCategoriesTable
|
|
26
|
+
context_object_name = "incident_categories"
|
|
27
|
+
filterset_class = IncidentCategoryFilterSet
|
|
28
|
+
model = IncidentCategory
|
|
29
29
|
|
|
30
30
|
paginate_by = 150
|
|
31
31
|
paginate_orphans = 20
|
|
@@ -46,7 +46,7 @@ class ComponentsViewList(SingleTableMixin, FilterView):
|
|
|
46
46
|
if request.htmx and not request.htmx.boosted:
|
|
47
47
|
template_name = "layouts/partials/partial_table_list_paginated.html"
|
|
48
48
|
else:
|
|
49
|
-
template_name = "pages/
|
|
49
|
+
template_name = "pages/incident_category_list.html"
|
|
50
50
|
|
|
51
51
|
return [template_name]
|
|
52
52
|
|
|
@@ -359,8 +359,8 @@ class _IncidentByDomainAnnotation(TypedDict):
|
|
|
359
359
|
def get_incidents_by_domain(
|
|
360
360
|
incidents: QuerySet[Incident],
|
|
361
361
|
) -> QuerySet[WithAnnotations[Group, _IncidentByDomainAnnotation]]:
|
|
362
|
-
return Group.objects.filter(
|
|
363
|
-
incidents_nb=Count("
|
|
362
|
+
return Group.objects.filter(incidentcategory__incident__in=incidents).annotate(
|
|
363
|
+
incidents_nb=Count("incidentcategory__incident", output_field=FloatField()),
|
|
364
364
|
)
|
|
365
365
|
|
|
366
366
|
|
|
@@ -34,13 +34,13 @@ class UserDetailView(CustomDetailView[User]):
|
|
|
34
34
|
"conversation_set",
|
|
35
35
|
queryset=Conversation.objects.not_incident_channel()
|
|
36
36
|
.exclude(tag="")
|
|
37
|
-
.filter(
|
|
37
|
+
.filter(incident_categories__isnull=False)
|
|
38
38
|
.distinct()
|
|
39
39
|
.prefetch_related(Prefetch("members")),
|
|
40
40
|
),
|
|
41
41
|
Prefetch(
|
|
42
42
|
"usergroup_set",
|
|
43
|
-
queryset=UserGroup.objects.filter(
|
|
43
|
+
queryset=UserGroup.objects.filter(incident_categories__isnull=False)
|
|
44
44
|
.distinct()
|
|
45
45
|
.prefetch_related(Prefetch("members")),
|
|
46
46
|
),
|
|
@@ -44,7 +44,7 @@ class IncidentListView(SingleTableMixin, FilterView):
|
|
|
44
44
|
paginate_by = 125
|
|
45
45
|
paginate_orphans = 15
|
|
46
46
|
queryset = Incident.objects.select_related(
|
|
47
|
-
"priority", "
|
|
47
|
+
"priority", "incident_category__group", "environment"
|
|
48
48
|
).order_by("-id")
|
|
49
49
|
|
|
50
50
|
def get_template_names(self) -> list[str]:
|
|
@@ -69,7 +69,7 @@ class IncidentListView(SingleTableMixin, FilterView):
|
|
|
69
69
|
"status",
|
|
70
70
|
"environment",
|
|
71
71
|
"priority",
|
|
72
|
-
"
|
|
72
|
+
"incident_category",
|
|
73
73
|
]
|
|
74
74
|
|
|
75
75
|
context["page_title"] = "Incidents List"
|
|
@@ -82,7 +82,7 @@ class IncidentStatisticsView(FilterView):
|
|
|
82
82
|
filterset_class = IncidentFilterSet
|
|
83
83
|
model = Incident
|
|
84
84
|
queryset = (
|
|
85
|
-
Incident.objects.select_related("priority", "
|
|
85
|
+
Incident.objects.select_related("priority", "incident_category__group", "environment")
|
|
86
86
|
.all()
|
|
87
87
|
.order_by("-id")
|
|
88
88
|
)
|
|
@@ -105,7 +105,7 @@ class IncidentStatisticsView(FilterView):
|
|
|
105
105
|
"status",
|
|
106
106
|
"environment",
|
|
107
107
|
"priority",
|
|
108
|
-
"
|
|
108
|
+
"incident_category",
|
|
109
109
|
]
|
|
110
110
|
context_data = weekly_dashboard_context(
|
|
111
111
|
self.request, context.get("incidents_filtered", [])
|
|
@@ -123,7 +123,7 @@ class DashboardView(generic.ListView[Incident]):
|
|
|
123
123
|
)
|
|
124
124
|
queryset = (
|
|
125
125
|
Incident.objects.filter(_status__lt=IncidentStatus.CLOSED)
|
|
126
|
-
.select_related("priority", "
|
|
126
|
+
.select_related("priority", "incident_category__group", "environment", "created_by")
|
|
127
127
|
.order_by("_status", "priority__value")
|
|
128
128
|
.annotate(latest_event_ts=Subquery(sub.values("event_ts")))
|
|
129
129
|
)
|
|
@@ -144,7 +144,7 @@ class IncidentDetailView(CustomDetailView[Incident]):
|
|
|
144
144
|
select_related = [
|
|
145
145
|
"priority",
|
|
146
146
|
"environment",
|
|
147
|
-
"
|
|
147
|
+
"incident_category__group",
|
|
148
148
|
"conversation",
|
|
149
149
|
"created_by",
|
|
150
150
|
]
|
|
@@ -155,7 +155,7 @@ class IncidentDetailView(CustomDetailView[Incident]):
|
|
|
155
155
|
"incidentupdate_set",
|
|
156
156
|
queryset=IncidentUpdate.objects.select_related(
|
|
157
157
|
"priority",
|
|
158
|
-
"
|
|
158
|
+
"incident_category__group",
|
|
159
159
|
"created_by__slack_user",
|
|
160
160
|
"created_by",
|
|
161
161
|
"commander",
|
firefighter/jira_app/client.py
CHANGED
|
@@ -273,7 +273,7 @@ class JiraClient:
|
|
|
273
273
|
name = jira_api_user.raw.get("displayName")
|
|
274
274
|
if not name or not isinstance(name, str):
|
|
275
275
|
logger.warning("User %s has no display name, using email as name", email)
|
|
276
|
-
name = email.split("@")[0]
|
|
276
|
+
name = email.split("@", maxsplit=1)[0]
|
|
277
277
|
try:
|
|
278
278
|
user: User = User.objects.create(
|
|
279
279
|
name=name,
|
|
@@ -7,7 +7,8 @@ from socket import socket
|
|
|
7
7
|
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
9
9
|
from django.conf import settings
|
|
10
|
-
from pythonjsonlogger.
|
|
10
|
+
from pythonjsonlogger.core import RESERVED_ATTRS
|
|
11
|
+
from pythonjsonlogger.json import JsonEncoder, JsonFormatter
|
|
11
12
|
|
|
12
13
|
if TYPE_CHECKING:
|
|
13
14
|
from logging import LogRecord
|
|
@@ -38,7 +38,7 @@ def trigger_oncall(
|
|
|
38
38
|
details = f"""Triggered from {APP_DISPLAY_NAME} incident #{incident.id} {f"by {triggered_by.full_name}" if triggered_by else ""}
|
|
39
39
|
Priority: {incident.priority}
|
|
40
40
|
Environment: {incident.environment}
|
|
41
|
-
|
|
41
|
+
Incident category: {incident.incident_category.group.name} - {incident.incident_category.name}
|
|
42
42
|
FireFighter page: {incident.status_page_url + "?utm_medium=FireFighter+PagerDuty&utm_source=PagerDuty+Incident&utm_campaign=OnCall+Message+In+Channel"}
|
|
43
43
|
Slack channel #{incident.slack_channel_name}: {incident.slack_channel_url}
|
|
44
44
|
|
firefighter/raid/admin.py
CHANGED
|
@@ -8,7 +8,6 @@ from firefighter.raid.models import (
|
|
|
8
8
|
FeatureTeam,
|
|
9
9
|
JiraTicket,
|
|
10
10
|
JiraTicketImpact,
|
|
11
|
-
RaidArea,
|
|
12
11
|
)
|
|
13
12
|
from firefighter.raid.resources import FeatureTeamResource
|
|
14
13
|
|
|
@@ -31,16 +30,6 @@ class JiraTicketAdmin(JiraIssueAdmin):
|
|
|
31
30
|
inlines = [JiraTicketImpactInline]
|
|
32
31
|
|
|
33
32
|
|
|
34
|
-
@admin.register(RaidArea)
|
|
35
|
-
class RaidAreaAdmin(admin.ModelAdmin[RaidArea]):
|
|
36
|
-
model = RaidArea
|
|
37
|
-
list_display = ["id", "name", "area"]
|
|
38
|
-
list_display_links = ["id", "name"]
|
|
39
|
-
list_filter = ["area"]
|
|
40
|
-
search_fields = ["id", "name"]
|
|
41
|
-
ordering = ["area", "name"]
|
|
42
|
-
|
|
43
|
-
|
|
44
33
|
@admin.register(FeatureTeam)
|
|
45
34
|
class FeatureTeamAdmin(ImportExportModelAdmin):
|
|
46
35
|
resource_class = FeatureTeamResource
|
firefighter/raid/client.py
CHANGED
|
@@ -52,8 +52,8 @@ class RaidJiraClient(JiraClient):
|
|
|
52
52
|
suggested_team_routing: str | None = None,
|
|
53
53
|
business_impact: str | None = None,
|
|
54
54
|
platform: str | None = None,
|
|
55
|
-
area: str | None = None,
|
|
56
55
|
environments: list[str] | None = None,
|
|
56
|
+
incident_category: str | None = None,
|
|
57
57
|
project: str | None = None,
|
|
58
58
|
) -> JiraObject:
|
|
59
59
|
description_addendum: list[str] = []
|
|
@@ -68,8 +68,6 @@ class RaidJiraClient(JiraClient):
|
|
|
68
68
|
if not 1 <= priority <= 5:
|
|
69
69
|
raise ValueError("Priority must be between 1 and 5")
|
|
70
70
|
priority_value = str(priority)
|
|
71
|
-
if area:
|
|
72
|
-
extra_args["customfield_10920"] = str(area)
|
|
73
71
|
if zoho_desk_ticket_id:
|
|
74
72
|
extra_args["customfield_10896"] = str(zoho_desk_ticket_id)
|
|
75
73
|
if zendesk_ticket_id:
|
|
@@ -96,6 +94,8 @@ class RaidJiraClient(JiraClient):
|
|
|
96
94
|
extra_args["customfield_10201"] = {"value": platform}
|
|
97
95
|
if environments:
|
|
98
96
|
extra_args["customfield_11049"] = [{"value": e} for e in environments]
|
|
97
|
+
if incident_category and settings.RAID_JIRA_INCIDENT_CATEGORY_FIELD:
|
|
98
|
+
extra_args[settings.RAID_JIRA_INCIDENT_CATEGORY_FIELD] = {"value": incident_category}
|
|
99
99
|
if len(description_addendum) > 0:
|
|
100
100
|
description_addendum_str = "\n *Additional Information* \n" + "\n".join(
|
|
101
101
|
description_addendum
|
firefighter/raid/forms.py
CHANGED
|
@@ -10,6 +10,8 @@ from slack_sdk.errors import SlackApiError
|
|
|
10
10
|
|
|
11
11
|
from firefighter.incidents.forms.create_incident import CreateIncidentFormBase
|
|
12
12
|
from firefighter.incidents.forms.select_impact import SelectImpactForm
|
|
13
|
+
from firefighter.incidents.forms.utils import GroupedModelChoiceField
|
|
14
|
+
from firefighter.incidents.models import IncidentCategory
|
|
13
15
|
from firefighter.incidents.models.priority import Priority
|
|
14
16
|
from firefighter.jira_app.client import (
|
|
15
17
|
JiraAPIError,
|
|
@@ -21,8 +23,9 @@ from firefighter.raid.messages import (
|
|
|
21
23
|
SlackMessageRaidCreatedIssue,
|
|
22
24
|
SlackMessageRaidModifiedIssue,
|
|
23
25
|
)
|
|
24
|
-
from firefighter.raid.models import FeatureTeam, JiraTicket
|
|
26
|
+
from firefighter.raid.models import FeatureTeam, JiraTicket
|
|
25
27
|
from firefighter.raid.service import (
|
|
28
|
+
CustomerIssueData,
|
|
26
29
|
create_issue_customer,
|
|
27
30
|
create_issue_documentation_request,
|
|
28
31
|
create_issue_feature_request,
|
|
@@ -77,7 +80,7 @@ class CreateNormalIncidentFormBase(CreateIncidentFormBase):
|
|
|
77
80
|
max_length=1200,
|
|
78
81
|
)
|
|
79
82
|
suggested_team_routing = forms.ModelChoiceField(
|
|
80
|
-
queryset=FeatureTeam.objects.only("name"),
|
|
83
|
+
queryset=FeatureTeam.objects.only("name").order_by("name"),
|
|
81
84
|
label="Feature Team or Train",
|
|
82
85
|
required=True,
|
|
83
86
|
)
|
|
@@ -89,7 +92,7 @@ class CreateNormalIncidentFormBase(CreateIncidentFormBase):
|
|
|
89
92
|
)
|
|
90
93
|
|
|
91
94
|
field_order = [
|
|
92
|
-
"
|
|
95
|
+
"incident_category",
|
|
93
96
|
"platform",
|
|
94
97
|
"title",
|
|
95
98
|
"description",
|
|
@@ -112,7 +115,11 @@ class CreateNormalIncidentFormBase(CreateIncidentFormBase):
|
|
|
112
115
|
|
|
113
116
|
|
|
114
117
|
class CreateNormalCustomerIncidentForm(CreateNormalIncidentFormBase):
|
|
115
|
-
|
|
118
|
+
incident_category = GroupedModelChoiceField(
|
|
119
|
+
choices_groupby="group",
|
|
120
|
+
queryset=IncidentCategory.objects.all().select_related("group").order_by("group__order", "name"),
|
|
121
|
+
label="Incident category"
|
|
122
|
+
)
|
|
116
123
|
zendesk_ticket_id = forms.CharField(
|
|
117
124
|
label="Zendesk Ticket ID", max_length=128, min_length=2, required=False
|
|
118
125
|
)
|
|
@@ -126,17 +133,21 @@ class CreateNormalCustomerIncidentForm(CreateNormalIncidentFormBase):
|
|
|
126
133
|
**kwargs: Never,
|
|
127
134
|
) -> None:
|
|
128
135
|
jira_user: JiraUser = get_jira_user_from_user(creator)
|
|
129
|
-
|
|
130
|
-
title=self.cleaned_data["title"],
|
|
131
|
-
description=self.cleaned_data["description"],
|
|
136
|
+
customer_data = CustomerIssueData(
|
|
132
137
|
priority=self.cleaned_data["priority"].value,
|
|
133
|
-
|
|
138
|
+
labels=[""],
|
|
134
139
|
platform=self.cleaned_data["platform"],
|
|
135
140
|
business_impact=str(get_business_impact(impacts_data)),
|
|
136
141
|
team_to_be_routed=self.cleaned_data["suggested_team_routing"],
|
|
137
|
-
area=
|
|
142
|
+
area=None,
|
|
138
143
|
zendesk_ticket_id=self.cleaned_data["zendesk_ticket_id"],
|
|
139
|
-
|
|
144
|
+
incident_category=self.cleaned_data["incident_category"].name,
|
|
145
|
+
)
|
|
146
|
+
issue_data = create_issue_customer(
|
|
147
|
+
title=self.cleaned_data["title"],
|
|
148
|
+
description=self.cleaned_data["description"],
|
|
149
|
+
reporter=jira_user.id,
|
|
150
|
+
issue_data=customer_data,
|
|
140
151
|
)
|
|
141
152
|
process_jira_issue(
|
|
142
153
|
issue_data, creator, jira_user=jira_user, impacts_data=impacts_data
|
|
@@ -144,6 +155,12 @@ class CreateNormalCustomerIncidentForm(CreateNormalIncidentFormBase):
|
|
|
144
155
|
|
|
145
156
|
|
|
146
157
|
class CreateRaidDocumentationRequestIncidentForm(CreateNormalIncidentFormBase):
|
|
158
|
+
incident_category = GroupedModelChoiceField(
|
|
159
|
+
choices_groupby="group",
|
|
160
|
+
queryset=IncidentCategory.objects.all().select_related("group").order_by("group__order", "name"),
|
|
161
|
+
label="Incident category"
|
|
162
|
+
)
|
|
163
|
+
|
|
147
164
|
def trigger_incident_workflow(
|
|
148
165
|
self,
|
|
149
166
|
creator: User,
|
|
@@ -167,6 +184,12 @@ class CreateRaidDocumentationRequestIncidentForm(CreateNormalIncidentFormBase):
|
|
|
167
184
|
|
|
168
185
|
|
|
169
186
|
class CreateRaidFeatureRequestIncidentForm(CreateNormalIncidentFormBase):
|
|
187
|
+
incident_category = GroupedModelChoiceField(
|
|
188
|
+
choices_groupby="group",
|
|
189
|
+
queryset=IncidentCategory.objects.all().select_related("group").order_by("group__order", "name"),
|
|
190
|
+
label="Incident category"
|
|
191
|
+
)
|
|
192
|
+
|
|
170
193
|
def trigger_incident_workflow(
|
|
171
194
|
self,
|
|
172
195
|
creator: User,
|
|
@@ -190,8 +213,10 @@ class CreateRaidFeatureRequestIncidentForm(CreateNormalIncidentFormBase):
|
|
|
190
213
|
|
|
191
214
|
|
|
192
215
|
class CreateRaidInternalIncidentForm(CreateNormalIncidentFormBase):
|
|
193
|
-
|
|
194
|
-
|
|
216
|
+
incident_category = GroupedModelChoiceField(
|
|
217
|
+
choices_groupby="group",
|
|
218
|
+
queryset=IncidentCategory.objects.all().select_related("group").order_by("group__order", "name"),
|
|
219
|
+
label="Incident category"
|
|
195
220
|
)
|
|
196
221
|
|
|
197
222
|
def trigger_incident_workflow(
|
|
@@ -210,7 +235,7 @@ class CreateRaidInternalIncidentForm(CreateNormalIncidentFormBase):
|
|
|
210
235
|
platform=self.cleaned_data["platform"],
|
|
211
236
|
business_impact=str(get_business_impact(impacts_data)),
|
|
212
237
|
team_to_be_routed=self.cleaned_data["suggested_team_routing"],
|
|
213
|
-
|
|
238
|
+
incident_category=self.cleaned_data["incident_category"].name,
|
|
214
239
|
labels=[""],
|
|
215
240
|
)
|
|
216
241
|
|
|
@@ -220,7 +245,11 @@ class CreateRaidInternalIncidentForm(CreateNormalIncidentFormBase):
|
|
|
220
245
|
|
|
221
246
|
|
|
222
247
|
class RaidCreateIncidentSellerForm(CreateNormalIncidentFormBase):
|
|
223
|
-
|
|
248
|
+
incident_category = GroupedModelChoiceField(
|
|
249
|
+
choices_groupby="group",
|
|
250
|
+
queryset=IncidentCategory.objects.all().select_related("group").order_by("group__order", "name"),
|
|
251
|
+
label="Incident category"
|
|
252
|
+
)
|
|
224
253
|
seller_contract_id = forms.CharField(
|
|
225
254
|
label="Seller Contract ID", max_length=128, min_length=0
|
|
226
255
|
)
|
|
@@ -248,7 +277,7 @@ class RaidCreateIncidentSellerForm(CreateNormalIncidentFormBase):
|
|
|
248
277
|
platform=self.cleaned_data["platform"],
|
|
249
278
|
business_impact=str(get_business_impact(impacts_data)),
|
|
250
279
|
team_to_be_routed=self.cleaned_data["suggested_team_routing"],
|
|
251
|
-
|
|
280
|
+
incident_category=self.cleaned_data["incident_category"].name,
|
|
252
281
|
zoho_desk_ticket_id=self.cleaned_data["zoho_desk_ticket_id"],
|
|
253
282
|
is_key_account=self.cleaned_data["is_key_account"],
|
|
254
283
|
is_seller_in_golden_list=self.cleaned_data["is_seller_in_golden_list"],
|
|
@@ -335,10 +364,15 @@ def alert_slack_new_jira_ticket(
|
|
|
335
364
|
message,
|
|
336
365
|
unfurl_links=False,
|
|
337
366
|
)
|
|
338
|
-
except SlackApiError:
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
367
|
+
except SlackApiError as e:
|
|
368
|
+
if e.response.get("error") == "messages_tab_disabled":
|
|
369
|
+
logger.warning(
|
|
370
|
+
f"User {reporter_user.slack_user} has disabled private messages from bots"
|
|
371
|
+
)
|
|
372
|
+
else:
|
|
373
|
+
logger.exception(
|
|
374
|
+
f"Couldn't send private message to reporter {reporter_user.slack_user}"
|
|
375
|
+
)
|
|
342
376
|
|
|
343
377
|
# Get the right channels from tags
|
|
344
378
|
channels = get_internal_alert_conversations(jira_ticket)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Generated by Django 4.2.23 on 2025-09-17 17:49
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
("raid", "0002_featureteam_remove_qualifierrotation_jira_user_and_more"),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.DeleteModel(
|
|
14
|
+
name="RaidArea",
|
|
15
|
+
),
|
|
16
|
+
]
|
firefighter/raid/models.py
CHANGED
|
@@ -58,25 +58,6 @@ class JiraTicketImpact(models.Model):
|
|
|
58
58
|
return f"{self.jira_ticket.key}: {self.impact}"
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
class RaidArea(models.Model):
|
|
62
|
-
id = models.AutoField(primary_key=True)
|
|
63
|
-
name = models.CharField(max_length=128)
|
|
64
|
-
area = models.CharField(
|
|
65
|
-
choices=(
|
|
66
|
-
("Sellers", "Sellers"),
|
|
67
|
-
("Internal", "Internal"),
|
|
68
|
-
("Customers", "Customers"),
|
|
69
|
-
),
|
|
70
|
-
max_length=32,
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
class Meta(TypedModelMeta):
|
|
74
|
-
unique_together = ("name", "area")
|
|
75
|
-
|
|
76
|
-
def __str__(self) -> str:
|
|
77
|
-
return self.name
|
|
78
|
-
|
|
79
|
-
|
|
80
61
|
class FeatureTeam(models.Model):
|
|
81
62
|
id = models.AutoField(primary_key=True)
|
|
82
63
|
name = models.CharField(max_length=80)
|
|
@@ -95,8 +76,8 @@ class FeatureTeam(models.Model):
|
|
|
95
76
|
|
|
96
77
|
@property
|
|
97
78
|
def get_team(self) -> str:
|
|
98
|
-
return "{self.name} {self.jira_project_key}"
|
|
79
|
+
return f"{self.name} {self.jira_project_key}"
|
|
99
80
|
|
|
100
81
|
@property
|
|
101
82
|
def get_key(self) -> str:
|
|
102
|
-
return "{self.jira_project_key}"
|
|
83
|
+
return f"{self.jira_project_key}"
|