firefighter-incident 0.0.13__py3-none-any.whl → 0.0.14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. firefighter/_version.py +16 -3
  2. firefighter/api/serializers.py +8 -8
  3. firefighter/api/urls.py +8 -1
  4. firefighter/api/views/_base.py +1 -1
  5. firefighter/api/views/components.py +5 -5
  6. firefighter/api/views/incidents.py +9 -9
  7. firefighter/firefighter/settings/components/raid.py +3 -0
  8. firefighter/incidents/admin.py +24 -24
  9. firefighter/incidents/factories.py +14 -5
  10. firefighter/incidents/forms/close_incident.py +4 -4
  11. firefighter/incidents/forms/create_incident.py +4 -4
  12. firefighter/incidents/forms/update_status.py +4 -4
  13. firefighter/incidents/menus.py +2 -2
  14. firefighter/incidents/migrations/0005_enable_from_p1_to_p5_priority.py +7 -5
  15. firefighter/incidents/migrations/0009_update_sla.py +7 -5
  16. firefighter/incidents/migrations/0020_create_incident_category_model.py +64 -0
  17. firefighter/incidents/migrations/0021_copy_component_data_to_incident_category.py +57 -0
  18. firefighter/incidents/migrations/0022_add_incident_category_fields.py +34 -0
  19. firefighter/incidents/migrations/0023_populate_incident_category_references.py +57 -0
  20. firefighter/incidents/migrations/0024_remove_component_fields_and_model.py +26 -0
  21. firefighter/incidents/migrations/0025_make_incident_category_required.py +24 -0
  22. firefighter/incidents/migrations/0026_alter_incidentcategory_options_and_more.py +39 -0
  23. firefighter/incidents/models/__init__.py +1 -1
  24. firefighter/incidents/models/group.py +1 -1
  25. firefighter/incidents/models/incident.py +15 -15
  26. firefighter/incidents/models/{component.py → incident_category.py} +30 -29
  27. firefighter/incidents/models/incident_update.py +3 -3
  28. firefighter/incidents/tables.py +9 -9
  29. firefighter/incidents/templates/layouts/partials/incident_card.html +1 -1
  30. firefighter/incidents/templates/layouts/partials/incident_timeline.html +2 -2
  31. firefighter/incidents/templates/pages/{component_detail.html → incident_category_detail.html} +13 -13
  32. firefighter/incidents/templates/pages/{component_list.html → incident_category_list.html} +2 -2
  33. firefighter/incidents/templates/pages/incident_detail.html +3 -3
  34. firefighter/incidents/urls.py +6 -6
  35. firefighter/incidents/views/components/details.py +9 -9
  36. firefighter/incidents/views/components/list.py +9 -9
  37. firefighter/incidents/views/reports.py +2 -2
  38. firefighter/incidents/views/users/details.py +2 -2
  39. firefighter/incidents/views/views.py +7 -7
  40. firefighter/jira_app/client.py +1 -1
  41. firefighter/logging/custom_json_formatter.py +2 -1
  42. firefighter/pagerduty/tasks/trigger_oncall.py +1 -1
  43. firefighter/raid/admin.py +0 -11
  44. firefighter/raid/client.py +3 -3
  45. firefighter/raid/forms.py +53 -19
  46. firefighter/raid/migrations/0003_delete_raidarea.py +16 -0
  47. firefighter/raid/models.py +2 -21
  48. firefighter/raid/serializers.py +5 -4
  49. firefighter/raid/service.py +29 -27
  50. firefighter/raid/signals/incident_created.py +4 -2
  51. firefighter/raid/utils.py +1 -1
  52. firefighter/raid/views/__init__.py +1 -1
  53. firefighter/raid/views/open_normal.py +2 -2
  54. firefighter/slack/admin.py +8 -8
  55. firefighter/slack/management/commands/switch_test_users.py +272 -0
  56. firefighter/slack/messages/slack_messages.py +5 -5
  57. firefighter/slack/migrations/0005_add_incident_categories_fields.py +33 -0
  58. firefighter/slack/migrations/0006_copy_components_to_incident_categories.py +57 -0
  59. firefighter/slack/migrations/0007_remove_components_fields.py +22 -0
  60. firefighter/slack/migrations/0008_alter_conversation_incident_categories_and_more.py +33 -0
  61. firefighter/slack/models/conversation.py +3 -3
  62. firefighter/slack/models/incident_channel.py +1 -1
  63. firefighter/slack/models/user.py +1 -1
  64. firefighter/slack/models/user_group.py +3 -3
  65. firefighter/slack/rules.py +1 -1
  66. firefighter/slack/signals/get_users.py +2 -2
  67. firefighter/slack/signals/incident_updated.py +1 -1
  68. firefighter/slack/utils.py +2 -2
  69. firefighter/slack/views/events/home.py +2 -2
  70. firefighter/slack/views/modals/base_modal/form_utils.py +15 -0
  71. firefighter/slack/views/modals/close.py +3 -3
  72. firefighter/slack/views/modals/open.py +25 -1
  73. firefighter/slack/views/modals/opening/check_current_incidents.py +2 -2
  74. firefighter/slack/views/modals/opening/details/critical.py +1 -1
  75. firefighter/slack/views/modals/opening/select_impact.py +5 -2
  76. firefighter/slack/views/modals/update_status.py +4 -4
  77. firefighter_fixtures/incidents/{components.json → incident_categories.json} +52 -52
  78. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.14.dist-info}/METADATA +2 -2
  79. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.14.dist-info}/RECORD +98 -77
  80. firefighter_tests/conftest.py +4 -5
  81. firefighter_tests/test_api/test_api_landbot.py +1 -1
  82. firefighter_tests/test_firefighter/test_sso.py +146 -0
  83. firefighter_tests/test_incidents/test_forms/test_form_utils.py +15 -15
  84. firefighter_tests/test_incidents/test_incident_urls.py +3 -3
  85. firefighter_tests/test_incidents/test_models/test_incident_category.py +165 -0
  86. firefighter_tests/test_incidents/test_models/test_incident_model.py +2 -2
  87. firefighter_tests/test_raid/test_priority_mapping.py +267 -0
  88. firefighter_tests/test_raid/test_raid_client.py +580 -0
  89. firefighter_tests/test_raid/test_raid_forms.py +795 -0
  90. firefighter_tests/test_raid/test_raid_models.py +185 -0
  91. firefighter_tests/test_raid/test_raid_serializers.py +507 -0
  92. firefighter_tests/test_raid/test_raid_service.py +442 -0
  93. firefighter_tests/test_raid/test_raid_views.py +196 -0
  94. firefighter_tests/test_slack/views/modals/test_close.py +6 -6
  95. firefighter_tests/test_slack/views/modals/test_update_status.py +4 -4
  96. firefighter_fixtures/raid/area.json +0 -1
  97. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.14.dist-info}/WHEEL +0 -0
  98. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.14.dist-info}/entry_points.txt +0 -0
  99. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.14.dist-info}/licenses/LICENSE +0 -0
@@ -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
- {{ component.name }}
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.change_components %}
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="Issue category information" id="component-information" %}
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 component.description %}
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"> {{ component.description|urlize|linebreaksbr }}</dd>
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"> {{ component.group.name }}</dd>
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 component" id="incident-responders-groups" %}
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 component.usergroups.all %}
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 component.conversations.all %}
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 components in "{{ component.group.name }}"</h2>
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 other_component in component.group.component_set.all %}
82
- {% if other_component.id != component.id %}
83
- <li><a href="{{ other_component.get_absolute_url }}" class="link"> {{ other_component.name }}</a></li>
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
- Issue category list <div role="status" class="hx-progress htmx-indicator inline">
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:components-list" %}{% endcomponent %}
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.component is not none %}
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.component.group }}</dd>
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.component.get_absolute_url }}">{{ incident.component }}</a></dd>
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
 
@@ -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 ComponentDetailView
7
- from firefighter.incidents.views.components.list import ComponentsViewList
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("component/", ComponentsViewList.as_view(), name="component-list"),
40
+ path("incident-category/", IncidentCategoriesViewList.as_view(), name="incident-category-list"),
41
41
  path(
42
- "component/<uuid:component_id>/",
43
- ComponentDetailView.as_view(),
44
- name="component-detail",
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 Component
9
+ from firefighter.incidents.models import IncidentCategory
10
10
 
11
11
  logger = logging.getLogger(__name__)
12
12
 
13
13
 
14
- class ComponentDetailView(CustomDetailView[Component]):
15
- template_name = "pages/component_detail.html"
16
- context_object_name = "component"
17
- pk_url_kwarg = "component_id"
18
- model = Component
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 = Component.objects.select_related(*select_related).prefetch_related(
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
- component: Component = context["component"]
27
+ incident_category: IncidentCategory = context["incident_category"]
28
28
 
29
29
  additional_context = {
30
- "page_title": f"{component.name} Component",
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 Component
11
- from firefighter.incidents.models.component import ComponentFilterSet
12
- from firefighter.incidents.tables import ComponentsTable
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 ComponentsViewList(SingleTableMixin, FilterView):
25
- table_class = ComponentsTable
26
- context_object_name = "components"
27
- filterset_class = ComponentFilterSet
28
- model = Component
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/component_list.html"
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(component__incident__in=incidents).annotate(
363
- incidents_nb=Count("component__incident", output_field=FloatField()),
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(components__isnull=False)
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(components__isnull=False)
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", "component__group", "environment"
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
- "component",
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", "component__group", "environment")
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
- "component",
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", "component__group", "environment", "created_by")
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
- "component__group",
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
- "component__group",
158
+ "incident_category__group",
159
159
  "created_by__slack_user",
160
160
  "created_by",
161
161
  "commander",
@@ -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.jsonlogger import RESERVED_ATTRS, JsonEncoder, JsonFormatter
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
- Issue category: {incident.component.group.name} - {incident.component.name}
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
@@ -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, RaidArea
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
- "area",
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
- area = forms.ModelChoiceField(queryset=RaidArea.objects.filter(area="Customers"))
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
- issue_data = create_issue_customer(
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
- reporter=jira_user.id,
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=self.cleaned_data["area"].name,
142
+ area=None,
138
143
  zendesk_ticket_id=self.cleaned_data["zendesk_ticket_id"],
139
- labels=[""],
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
- area = forms.ModelChoiceField(
194
- queryset=RaidArea.objects.filter(area="Internal").order_by("name")
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
- area=self.cleaned_data["area"].name,
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
- area = forms.ModelChoiceField(queryset=RaidArea.objects.filter(area="Sellers"))
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
- area=self.cleaned_data["area"].name,
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
- logger.exception(
340
- f"Couldn't send private message to reporter {reporter_user.slack_user}"
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
+ ]
@@ -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}"