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.
Files changed (136) hide show
  1. firefighter/_version.py +16 -3
  2. firefighter/api/serializers.py +17 -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/confluence/signals/incident_updated.py +2 -2
  8. firefighter/firefighter/settings/components/raid.py +3 -0
  9. firefighter/incidents/admin.py +24 -24
  10. firefighter/incidents/enums.py +22 -2
  11. firefighter/incidents/factories.py +14 -5
  12. firefighter/incidents/forms/close_incident.py +4 -4
  13. firefighter/incidents/forms/closure_reason.py +45 -0
  14. firefighter/incidents/forms/create_incident.py +4 -4
  15. firefighter/incidents/forms/unified_incident.py +406 -0
  16. firefighter/incidents/forms/update_status.py +91 -5
  17. firefighter/incidents/menus.py +2 -2
  18. firefighter/incidents/migrations/0005_enable_from_p1_to_p5_priority.py +7 -5
  19. firefighter/incidents/migrations/0009_update_sla.py +7 -5
  20. firefighter/incidents/migrations/0020_create_incident_category_model.py +64 -0
  21. firefighter/incidents/migrations/0021_copy_component_data_to_incident_category.py +57 -0
  22. firefighter/incidents/migrations/0022_add_incident_category_fields.py +34 -0
  23. firefighter/incidents/migrations/0023_populate_incident_category_references.py +57 -0
  24. firefighter/incidents/migrations/0024_remove_component_fields_and_model.py +26 -0
  25. firefighter/incidents/migrations/0025_make_incident_category_required.py +24 -0
  26. firefighter/incidents/migrations/0026_alter_incidentcategory_options_and_more.py +39 -0
  27. firefighter/incidents/migrations/0027_add_closure_fields.py +40 -0
  28. firefighter/incidents/migrations/0028_add_closure_reason_constraint.py +33 -0
  29. firefighter/incidents/migrations/0029_add_custom_fields_to_incident.py +22 -0
  30. firefighter/incidents/models/__init__.py +1 -1
  31. firefighter/incidents/models/group.py +1 -1
  32. firefighter/incidents/models/incident.py +47 -20
  33. firefighter/incidents/models/{component.py → incident_category.py} +30 -29
  34. firefighter/incidents/models/incident_update.py +3 -3
  35. firefighter/incidents/static/css/main.min.css +1 -1
  36. firefighter/incidents/tables.py +9 -9
  37. firefighter/incidents/templates/layouts/partials/incident_card.html +1 -1
  38. firefighter/incidents/templates/layouts/partials/incident_timeline.html +2 -2
  39. firefighter/incidents/templates/layouts/partials/status_pill.html +1 -1
  40. firefighter/incidents/templates/pages/{component_detail.html → incident_category_detail.html} +13 -13
  41. firefighter/incidents/templates/pages/{component_list.html → incident_category_list.html} +2 -2
  42. firefighter/incidents/templates/pages/incident_detail.html +3 -3
  43. firefighter/incidents/urls.py +6 -6
  44. firefighter/incidents/views/components/details.py +9 -9
  45. firefighter/incidents/views/components/list.py +9 -9
  46. firefighter/incidents/views/reports.py +5 -5
  47. firefighter/incidents/views/users/details.py +2 -2
  48. firefighter/incidents/views/views.py +7 -7
  49. firefighter/jira_app/client.py +1 -1
  50. firefighter/logging/custom_json_formatter.py +2 -1
  51. firefighter/pagerduty/tasks/trigger_oncall.py +1 -1
  52. firefighter/raid/admin.py +0 -11
  53. firefighter/raid/apps.py +9 -26
  54. firefighter/raid/client.py +5 -5
  55. firefighter/raid/forms.py +84 -213
  56. firefighter/raid/migrations/0003_delete_raidarea.py +16 -0
  57. firefighter/raid/models.py +2 -21
  58. firefighter/raid/serializers.py +5 -4
  59. firefighter/raid/service.py +29 -27
  60. firefighter/raid/signals/incident_created.py +42 -15
  61. firefighter/raid/signals/incident_updated.py +3 -2
  62. firefighter/raid/utils.py +1 -1
  63. firefighter/raid/views/__init__.py +1 -1
  64. firefighter/slack/admin.py +8 -8
  65. firefighter/slack/management/commands/switch_test_users.py +272 -0
  66. firefighter/slack/messages/slack_messages.py +24 -9
  67. firefighter/slack/migrations/0005_add_incident_categories_fields.py +33 -0
  68. firefighter/slack/migrations/0006_copy_components_to_incident_categories.py +57 -0
  69. firefighter/slack/migrations/0007_remove_components_fields.py +22 -0
  70. firefighter/slack/migrations/0008_alter_conversation_incident_categories_and_more.py +33 -0
  71. firefighter/slack/models/conversation.py +3 -3
  72. firefighter/slack/models/incident_channel.py +1 -1
  73. firefighter/slack/models/user.py +1 -1
  74. firefighter/slack/models/user_group.py +3 -3
  75. firefighter/slack/rules.py +2 -2
  76. firefighter/slack/signals/create_incident_conversation.py +6 -0
  77. firefighter/slack/signals/get_users.py +2 -2
  78. firefighter/slack/signals/incident_updated.py +8 -2
  79. firefighter/slack/utils.py +2 -2
  80. firefighter/slack/views/events/home.py +2 -2
  81. firefighter/slack/views/modals/__init__.py +4 -0
  82. firefighter/slack/views/modals/base_modal/form_utils.py +78 -0
  83. firefighter/slack/views/modals/close.py +18 -5
  84. firefighter/slack/views/modals/closure_reason.py +193 -0
  85. firefighter/slack/views/modals/open.py +83 -12
  86. firefighter/slack/views/modals/opening/check_current_incidents.py +2 -2
  87. firefighter/slack/views/modals/opening/details/unified.py +203 -0
  88. firefighter/slack/views/modals/opening/select_impact.py +5 -2
  89. firefighter/slack/views/modals/opening/set_details.py +3 -2
  90. firefighter/slack/views/modals/postmortem.py +10 -2
  91. firefighter/slack/views/modals/update_status.py +32 -6
  92. firefighter/slack/views/modals/utils.py +51 -0
  93. firefighter_fixtures/incidents/{components.json → incident_categories.json} +52 -52
  94. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/METADATA +2 -2
  95. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/RECORD +133 -88
  96. firefighter_tests/conftest.py +4 -5
  97. firefighter_tests/test_api/test_api_landbot.py +1 -1
  98. firefighter_tests/test_firefighter/test_sso.py +146 -0
  99. firefighter_tests/test_incidents/test_enums.py +100 -0
  100. firefighter_tests/test_incidents/test_forms/conftest.py +179 -0
  101. firefighter_tests/test_incidents/test_forms/test_closure_reason.py +91 -0
  102. firefighter_tests/test_incidents/test_forms/test_form_utils.py +15 -15
  103. firefighter_tests/test_incidents/test_forms/test_unified_incident_form.py +570 -0
  104. firefighter_tests/test_incidents/test_forms/test_unified_incident_form_integration.py +581 -0
  105. firefighter_tests/test_incidents/test_forms/test_unified_incident_form_p4_p5.py +410 -0
  106. firefighter_tests/test_incidents/test_forms/test_update_status_workflow.py +343 -0
  107. firefighter_tests/test_incidents/test_forms/test_workflow_transitions.py +167 -0
  108. firefighter_tests/test_incidents/test_incident_urls.py +3 -3
  109. firefighter_tests/test_incidents/test_models/test_incident_category.py +165 -0
  110. firefighter_tests/test_incidents/test_models/test_incident_model.py +70 -2
  111. firefighter_tests/test_raid/conftest.py +154 -0
  112. firefighter_tests/test_raid/test_p1_p3_jira_fields.py +372 -0
  113. firefighter_tests/test_raid/test_priority_mapping.py +267 -0
  114. firefighter_tests/test_raid/test_raid_client.py +580 -0
  115. firefighter_tests/test_raid/test_raid_forms.py +552 -0
  116. firefighter_tests/test_raid/test_raid_models.py +185 -0
  117. firefighter_tests/test_raid/test_raid_serializers.py +507 -0
  118. firefighter_tests/test_raid/test_raid_service.py +442 -0
  119. firefighter_tests/test_raid/test_raid_signals.py +187 -0
  120. firefighter_tests/test_raid/test_raid_views.py +196 -0
  121. firefighter_tests/test_slack/messages/__init__.py +0 -0
  122. firefighter_tests/test_slack/messages/test_slack_messages.py +367 -0
  123. firefighter_tests/test_slack/views/modals/conftest.py +140 -0
  124. firefighter_tests/test_slack/views/modals/test_close.py +71 -9
  125. firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +138 -0
  126. firefighter_tests/test_slack/views/modals/test_form_utils_multiple_choice.py +249 -0
  127. firefighter_tests/test_slack/views/modals/test_open.py +146 -2
  128. firefighter_tests/test_slack/views/modals/test_opening_unified.py +421 -0
  129. firefighter_tests/test_slack/views/modals/test_update_status.py +331 -7
  130. firefighter_tests/test_slack/views/modals/test_utils.py +135 -0
  131. firefighter/raid/views/open_normal.py +0 -139
  132. firefighter/slack/views/modals/opening/details/critical.py +0 -88
  133. firefighter_fixtures/raid/area.json +0 -1
  134. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/WHEEL +0 -0
  135. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/entry_points.txt +0 -0
  136. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/licenses/LICENSE +0 -0
@@ -6,7 +6,7 @@ import django_tables2 as tables
6
6
  from django.conf import settings
7
7
 
8
8
  from firefighter.firefighter.tables_utils import BASE_TABLE_ATTRS
9
- from firefighter.incidents.models import Component, Incident
9
+ from firefighter.incidents.models import Incident, IncidentCategory
10
10
  from firefighter.incidents.models.incident import IncidentStatus
11
11
 
12
12
  if TYPE_CHECKING:
@@ -23,8 +23,8 @@ class IncidentTable(tables.Table):
23
23
  "priority",
24
24
  "status",
25
25
  "environment",
26
- "component",
27
- "component__group",
26
+ "incident_category",
27
+ "incident_category__group",
28
28
  "created_at",
29
29
  )
30
30
  order_by = "-id"
@@ -63,13 +63,13 @@ class IncidentTable(tables.Table):
63
63
  attrs={"td": {"class": "table-td text-center"}},
64
64
  )
65
65
  environment = tables.Column(attrs={"td": {"class": "table-td text-center"}})
66
- component = tables.Column(attrs={"td": {"class": "table-td text-center"}})
67
- component__group = tables.Column(attrs={"td": {"class": "table-td text-center"}})
66
+ incident_category = tables.Column(attrs={"td": {"class": "table-td text-center"}})
67
+ incident_category__group = tables.Column(attrs={"td": {"class": "table-td text-center"}})
68
68
 
69
69
 
70
- class ComponentsTable(tables.Table):
70
+ class IncidentCategoriesTable(tables.Table):
71
71
  class Meta:
72
- model = Component
72
+ model = IncidentCategory
73
73
  template_name = "incidents/table.html"
74
74
  fields = (
75
75
  "name",
@@ -93,12 +93,12 @@ class ComponentsTable(tables.Table):
93
93
  )
94
94
 
95
95
  @staticmethod
96
- def render_mtbf(record: Component, *args: Any) -> str:
96
+ def render_mtbf(record: IncidentCategory, *args: Any) -> str:
97
97
  mtbf: timedelta | None = record.mtbf # type: ignore[attr-defined]
98
98
  return str(mtbf).split(".", maxsplit=1)[0] if mtbf else "N/A"
99
99
 
100
100
  @staticmethod
101
- def render_incident_count(record: Component, *args: Any) -> int:
101
+ def render_incident_count(record: IncidentCategory, *args: Any) -> int:
102
102
  return int(record.incident_count) if record.incident_count else 0 # type: ignore[attr-defined]
103
103
 
104
104
  group__name = tables.Column(verbose_name="Group")
@@ -14,7 +14,7 @@
14
14
  {% include "./environment_pill.html" with environment=incident.environment only %}
15
15
  {% endif %}
16
16
  <span class="inline-flex font-semibold rounded-full px-2 text-xs leading-5 bg-neutral-300 text-neutral-600 ">
17
- {{ incident.component.name }} ({{ incident.component.group.name }})
17
+ {{ incident.incident_category.name }} ({{ incident.incident_category.group.name }})
18
18
  </span>
19
19
  {% include "./priority_pill.html" with priority=incident.priority only %}
20
20
  <p class="mt-2 text-neutral-500 dark:text-neutral-200 text-sm line-clamp-3">{{ incident.description|truncatechars:150 }}</p>
@@ -111,13 +111,13 @@
111
111
  Priority changed to: {{ incident_update.priority }}
112
112
  </p>
113
113
  {% endif %}
114
- {% if incident_update.component %}
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.component }}
120
+ {{ incident_update.incident_category }}
121
121
  </p>
122
122
  {% endif %}
123
123
  {% if incident_update.communication_lead or incident_update.commander %}
@@ -6,7 +6,7 @@
6
6
  {% endif %}
7
7
  {% if status.value == IncidentStatus.CLOSED %}
8
8
  bg-neutral-300 text-neutral-600
9
- {% elif status.value == IncidentStatus.FIXED %}
9
+ {% elif status.value == IncidentStatus.MITIGATED %}
10
10
  bg-success text-success-content
11
11
  {% elif status.value == IncidentStatus.POST_MORTEM %}
12
12
  bg-indigo-100 text-indigo-800
@@ -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
 
@@ -368,7 +368,7 @@ def get_incident_time_to_by_priority(
368
368
  incidents: QuerySet[Incident], time_to: str = "time_to_fix"
369
369
  ) -> tuple[dict[str, str], QuerySet[Priority]]:
370
370
  ttr_q = (
371
- Q(incident___status__gte=IncidentStatus.FIXED)
371
+ Q(incident___status__gte=IncidentStatus.MITIGATED)
372
372
  & Q(metric_type__type=time_to)
373
373
  & Q(duration__gte=timedelta(seconds=1))
374
374
  & Q(incident__in=incidents)
@@ -518,12 +518,12 @@ def get_incident_by_status_chart_data(
518
518
  ),
519
519
  incident_fixing=Count(
520
520
  "_status",
521
- filter=Q(_status=IncidentStatus.FIXING),
521
+ filter=Q(_status=IncidentStatus.MITIGATING),
522
522
  output_field=FloatField(),
523
523
  ),
524
524
  incident_fixed=Count(
525
525
  "_status",
526
- filter=Q(_status=IncidentStatus.FIXED),
526
+ filter=Q(_status=IncidentStatus.MITIGATED),
527
527
  output_field=FloatField(),
528
528
  ),
529
529
  incident_postmortem=Count(
@@ -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
firefighter/raid/apps.py CHANGED
@@ -20,35 +20,18 @@ class RaidConfig(AppConfig):
20
20
  incident_created,
21
21
  incident_updated,
22
22
  )
23
- from firefighter.raid.views.open_normal import (
24
- OpeningRaidCustomerModal,
25
- OpeningRaidDocumentationRequestModal,
26
- OpeningRaidFeatureRequestModal,
27
- OpeningRaidInternalModal,
28
- OpeningRaidSellerModal,
29
- )
30
23
  from firefighter.slack.views.modals.open import INCIDENT_TYPES
24
+ from firefighter.slack.views.modals.opening.details.unified import (
25
+ OpeningUnifiedModal,
26
+ )
31
27
 
28
+ # Use unified form for all normal incidents (P4-P5)
29
+ # This replaces the previous 5 separate forms (Customer/Seller/Internal/Doc/Feature)
30
+ # STEP 3 (incident type selection) will be automatically hidden since len() == 1
32
31
  INCIDENT_TYPES["normal"] = {
33
- "CUSTOMER": {
34
- "label": "Customer",
35
- "slack_form": OpeningRaidCustomerModal,
36
- },
37
- "SELLER": {
38
- "label": "Seller",
39
- "slack_form": OpeningRaidSellerModal,
40
- },
41
- "INTERNAL": {
42
- "label": "Internal",
43
- "slack_form": OpeningRaidInternalModal,
44
- },
45
- "DOCUMENTATION_REQUEST": {
46
- "label": "Documentation request",
47
- "slack_form": OpeningRaidDocumentationRequestModal,
48
- },
49
- "FEATURE_REQUEST": {
50
- "label": "Feature request",
51
- "slack_form": OpeningRaidFeatureRequestModal,
32
+ "normal": {
33
+ "label": "Normal",
34
+ "slack_form": OpeningUnifiedModal,
52
35
  },
53
36
  }
54
37
 
@@ -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:
@@ -79,9 +77,9 @@ class RaidJiraClient(JiraClient):
79
77
  f"Seller link to TOOLBOX: {TOOLBOX_URL}?seller_id={seller_contract_id}"
80
78
  )
81
79
  extra_args["customfield_10908"] = str(seller_contract_id)
82
- if is_seller_in_golden_list:
80
+ if is_seller_in_golden_list is True:
83
81
  labels = [*labels, "goldenList"]
84
- if is_key_account:
82
+ if is_key_account is True:
85
83
  labels = [*labels, "keyAccount"]
86
84
  if suggested_team_routing and suggested_team_routing != "SBI":
87
85
  description_addendum.append(
@@ -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