firefighter-incident 0.0.1rc2__py3-none-any.whl → 0.0.3__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 (213) hide show
  1. firefighter/_version.py +9 -4
  2. firefighter/api/admin.py +31 -12
  3. firefighter/api/migrations/0003_alter_apitokenproxy_options.py +30 -0
  4. firefighter/api/models.py +2 -2
  5. firefighter/api/renderer.py +1 -1
  6. firefighter/api/serializers.py +6 -4
  7. firefighter/api/urls.py +13 -19
  8. firefighter/components/avatar/avatar.py +24 -7
  9. firefighter/components/card/card.py +9 -4
  10. firefighter/components/export_button/export_button.html +2 -2
  11. firefighter/components/export_button/export_button.py +26 -7
  12. firefighter/components/form/form.html +1 -1
  13. firefighter/components/form/form.py +15 -12
  14. firefighter/components/form_field/form_field.html +4 -4
  15. firefighter/components/form_field/form_field.py +20 -12
  16. firefighter/components/messages/messages.html +39 -49
  17. firefighter/components/messages/messages.py +10 -5
  18. firefighter/components/modal/modal.html +20 -25
  19. firefighter/components/modal/modal.py +24 -9
  20. firefighter/confluence/models.py +2 -0
  21. firefighter/confluence/service.py +3 -3
  22. firefighter/confluence/tasks/archive_postmortems.py +7 -2
  23. firefighter/confluence/tasks/sync_postmortems.py +1 -1
  24. firefighter/confluence/tasks/sync_runbooks.py +3 -3
  25. firefighter/confluence/templates/oncall_team.xml +3 -3
  26. firefighter/confluence/templates/pages/runbook_list.html +4 -5
  27. firefighter/confluence/utils.py +4 -1
  28. firefighter/firefighter/filters.py +3 -2
  29. firefighter/firefighter/http_client.py +7 -9
  30. firefighter/firefighter/management/commands/task.py +1 -1
  31. firefighter/firefighter/settings/__init__.py +1 -0
  32. firefighter/firefighter/settings/components/common.py +8 -3
  33. firefighter/firefighter/settings/components/logging.py +1 -1
  34. firefighter/firefighter/settings/components/raid.py +3 -3
  35. firefighter/firefighter/settings/environments/dev.py +1 -3
  36. firefighter/firefighter/settings/environments/prod.py +1 -0
  37. firefighter/firefighter/sso.py +1 -1
  38. firefighter/firefighter/templates/admin/base.html +3 -2
  39. firefighter/firefighter/templates/admin/login.html +2 -2
  40. firefighter/firefighter/templates/admin/send_message_conversation.html +4 -4
  41. firefighter/firefighter/urls.py +7 -8
  42. firefighter/firefighter/utils.py +1 -3
  43. firefighter/firefighter/wsgi.py +2 -1
  44. firefighter/incidents/admin.py +11 -7
  45. firefighter/incidents/factories.py +33 -32
  46. firefighter/incidents/forms/edit.py +33 -0
  47. firefighter/incidents/forms/select_impact.py +4 -11
  48. firefighter/incidents/forms/update_key_events.py +1 -1
  49. firefighter/incidents/forms/update_roles.py +1 -1
  50. firefighter/incidents/forms/utils.py +3 -3
  51. firefighter/incidents/menus.py +5 -4
  52. firefighter/incidents/migrations/0002_alter_severity_name_alter_user_password_featureteam.py +35 -0
  53. firefighter/incidents/migrations/0003_delete_featureteam.py +16 -0
  54. firefighter/incidents/migrations/0004_incidentupdate_environment.py +27 -0
  55. firefighter/incidents/migrations/0005_enable_from_p1_to_p5_priority.py +30 -0
  56. firefighter/incidents/migrations/0006_update_group_names.py +102 -0
  57. firefighter/incidents/migrations/0007_update_component_name.py +148 -0
  58. firefighter/incidents/migrations/0008_impact_level.py +273 -0
  59. firefighter/incidents/models/component.py +8 -11
  60. firefighter/incidents/models/group.py +1 -0
  61. firefighter/incidents/models/impact.py +42 -12
  62. firefighter/incidents/models/incident.py +14 -11
  63. firefighter/incidents/models/incident_cost.py +2 -2
  64. firefighter/incidents/models/incident_membership.py +5 -5
  65. firefighter/incidents/models/incident_update.py +14 -5
  66. firefighter/incidents/models/metric_type.py +2 -2
  67. firefighter/incidents/signals.py +0 -5
  68. firefighter/incidents/static/css/main.min.css +1 -1
  69. firefighter/incidents/static/css/tailwind.css +18 -34
  70. firefighter/incidents/static/js/main.min.js +12 -12
  71. firefighter/incidents/tables.py +1 -5
  72. firefighter/incidents/tasks/updateoncall.py +1 -3
  73. firefighter/incidents/templates/incidents/errors/base.html +2 -1
  74. firefighter/incidents/templates/incidents/filter.html +33 -33
  75. firefighter/incidents/templates/incidents/table/priority_column.html +1 -1
  76. firefighter/incidents/templates/incidents/table.html +2 -3
  77. firefighter/incidents/templates/incidents/widgets/form_container.html +60 -61
  78. firefighter/incidents/templates/incidents/widgets/grouped_checkbox_nested.html +52 -52
  79. firefighter/incidents/templates/layouts/index.html +3 -3
  80. firefighter/incidents/templates/layouts/partials/created_at_help.html +2 -3
  81. firefighter/incidents/templates/layouts/partials/environment_pill.html +6 -6
  82. firefighter/incidents/templates/layouts/partials/footer.html +4 -7
  83. firefighter/incidents/templates/layouts/partials/header.html +41 -31
  84. firefighter/incidents/templates/layouts/partials/incident_card.html +6 -6
  85. firefighter/incidents/templates/layouts/partials/incident_metrics.html +1 -2
  86. firefighter/incidents/templates/layouts/partials/incident_timeline.html +35 -6
  87. firefighter/incidents/templates/layouts/partials/incident_update_key_events_view.html +3 -3
  88. firefighter/incidents/templates/layouts/partials/incident_update_key_events_view_modal.html +5 -7
  89. firefighter/incidents/templates/layouts/partials/partial_table_list_paginated.html +8 -9
  90. firefighter/incidents/templates/layouts/partials/priority_pill.html +1 -1
  91. firefighter/incidents/templates/layouts/partials/status_pill.html +15 -15
  92. firefighter/incidents/templates/layouts/partials/table.html +3 -3
  93. firefighter/incidents/templates/layouts/partials/user_card.html +3 -4
  94. firefighter/incidents/templates/layouts/partials/user_tooltip.html +1 -1
  95. firefighter/incidents/templates/layouts/view_filters.html +9 -9
  96. firefighter/incidents/templates/pages/component_detail.html +9 -9
  97. firefighter/incidents/templates/pages/component_list.html +5 -7
  98. firefighter/incidents/templates/pages/dashboard.html +4 -4
  99. firefighter/incidents/templates/pages/docs_metrics.html +43 -41
  100. firefighter/incidents/templates/pages/incident_create.html +9 -10
  101. firefighter/incidents/templates/pages/incident_detail.html +52 -46
  102. firefighter/incidents/templates/pages/incident_list.html +7 -7
  103. firefighter/incidents/templates/pages/incident_role_types_detail.html +3 -5
  104. firefighter/incidents/templates/pages/incident_role_types_list.html +3 -3
  105. firefighter/incidents/templates/pages/incident_statistics.html +10 -10
  106. firefighter/incidents/templates/pages/incident_statistics_partial.html +7 -11
  107. firefighter/incidents/templates/pages/incident_update_key_events_form.html +1 -1
  108. firefighter/incidents/templates/pages/user_detail.html +15 -15
  109. firefighter/incidents/views/docs/role_types.py +2 -1
  110. firefighter/incidents/views/errors.py +15 -16
  111. firefighter/incidents/views/reports.py +8 -8
  112. firefighter/incidents/views/users/details.py +1 -4
  113. firefighter/jira_app/client.py +3 -3
  114. firefighter/jira_app/models.py +2 -2
  115. firefighter/logging/custom_json_formatter.py +2 -2
  116. firefighter/pagerduty/models.py +9 -1
  117. firefighter/pagerduty/tasks/__init__.py +1 -0
  118. firefighter/pagerduty/tasks/trigger_oncall.py +7 -9
  119. firefighter/pagerduty/templates/pages/oncall_list.html +7 -7
  120. firefighter/pagerduty/templates/pages/oncall_trigger.html +1 -1
  121. firefighter/pagerduty/templates/partials/trigger_oncall_form_view.html +3 -3
  122. firefighter/pagerduty/templates/partials/trigger_oncall_form_view_modal.html +5 -7
  123. firefighter/raid/admin.py +13 -87
  124. firefighter/raid/apps.py +0 -1
  125. firefighter/raid/client.py +12 -7
  126. firefighter/raid/forms.py +5 -17
  127. firefighter/raid/messages.py +6 -90
  128. firefighter/raid/migrations/0002_featureteam_remove_qualifierrotation_jira_user_and_more.py +36 -0
  129. firefighter/raid/models.py +28 -46
  130. firefighter/raid/resources.py +15 -0
  131. firefighter/raid/service.py +5 -28
  132. firefighter/raid/signals/incident_created.py +2 -13
  133. firefighter/raid/tasks/__init__.py +0 -3
  134. firefighter/raid/views/open_normal.py +1 -1
  135. firefighter/slack/admin.py +39 -3
  136. firefighter/slack/factories.py +32 -32
  137. firefighter/slack/messages/slack_messages.py +47 -33
  138. firefighter/slack/migrations/0002_usergroup_tag.py +22 -0
  139. firefighter/slack/migrations/0003_alter_usergroup_tag.py +22 -0
  140. firefighter/slack/models/conversation.py +3 -3
  141. firefighter/slack/models/incident_channel.py +2 -2
  142. firefighter/slack/models/message.py +4 -4
  143. firefighter/slack/models/sos.py +2 -2
  144. firefighter/slack/models/user_group.py +6 -0
  145. firefighter/slack/rules.py +3 -4
  146. firefighter/slack/signals/create_incident_conversation.py +2 -1
  147. firefighter/slack/signals/get_users.py +29 -3
  148. firefighter/slack/signals/incident_updated.py +2 -2
  149. firefighter/slack/slack_app.py +18 -2
  150. firefighter/slack/slack_incident_context.py +8 -8
  151. firefighter/slack/tasks/fetch_conversations_members.py +1 -1
  152. firefighter/slack/tasks/send_reminders.py +1 -1
  153. firefighter/slack/tasks/update_usergroups_members.py +1 -1
  154. firefighter/slack/views/events/commands.py +3 -0
  155. firefighter/slack/views/events/home.py +22 -20
  156. firefighter/slack/views/modals/__init__.py +2 -0
  157. firefighter/slack/views/modals/base_modal/base.py +2 -2
  158. firefighter/slack/views/modals/base_modal/form_utils.py +6 -10
  159. firefighter/slack/views/modals/base_modal/mixins.py +1 -2
  160. firefighter/slack/views/modals/close.py +15 -13
  161. firefighter/slack/views/modals/downgrade_workflow.py +13 -17
  162. firefighter/slack/views/modals/edit.py +117 -0
  163. firefighter/slack/views/modals/key_event_message.py +9 -9
  164. firefighter/slack/views/modals/open.py +21 -21
  165. firefighter/slack/views/modals/opening/check_current_incidents.py +3 -3
  166. firefighter/slack/views/modals/opening/details/critical.py +7 -5
  167. firefighter/slack/views/modals/opening/select_impact.py +11 -14
  168. firefighter/slack/views/modals/opening/set_details.py +4 -2
  169. firefighter/slack/views/modals/opening/types.py +1 -1
  170. firefighter/slack/views/modals/postmortem.py +16 -20
  171. firefighter/slack/views/modals/select.py +1 -1
  172. firefighter/slack/views/modals/status.py +17 -21
  173. firefighter/slack/views/modals/trigger_oncall.py +19 -21
  174. firefighter/slack/views/modals/update.py +5 -0
  175. firefighter/slack/views/modals/update_status.py +7 -5
  176. firefighter_fixtures/incidents/components.json +619 -491
  177. firefighter_fixtures/incidents/groups.json +64 -119
  178. firefighter_fixtures/incidents/impact_level.json +124 -47
  179. {firefighter_incident-0.0.1rc2.dist-info → firefighter_incident-0.0.3.dist-info}/METADATA +13 -8
  180. {firefighter_incident-0.0.1rc2.dist-info → firefighter_incident-0.0.3.dist-info}/RECORD +210 -195
  181. {firefighter_incident-0.0.1rc2.dist-info → firefighter_incident-0.0.3.dist-info}/WHEEL +1 -1
  182. firefighter_tests/conftest.py +8 -9
  183. firefighter_tests/test_api/test_api_urls.py +4 -4
  184. firefighter_tests/test_firefighter/test_urls.py +12 -12
  185. firefighter_tests/test_incidents/test_forms/test_form_select_impact.py +27 -27
  186. firefighter_tests/test_incidents/test_forms/test_form_utils.py +4 -5
  187. firefighter_tests/test_incidents/test_forms/test_update_key_events.py +3 -3
  188. firefighter_tests/test_incidents/test_incident_urls.py +10 -10
  189. firefighter_tests/test_incidents/test_models/test_incident_model.py +1 -1
  190. firefighter_tests/test_incidents/test_utils/test_date_utils.py +3 -2
  191. firefighter_tests/test_incidents/test_views/test_incident_detail_view.py +3 -3
  192. firefighter_tests/test_incidents/test_views/test_index_view.py +6 -6
  193. firefighter_tests/test_raid/test_raid_client_users.py +12 -12
  194. firefighter_tests/test_slack/conftest.py +6 -6
  195. firefighter_tests/test_slack/test_models/test_conversations.py +2 -2
  196. firefighter_tests/test_slack/test_models/test_incident_channel.py +55 -46
  197. firefighter_tests/test_slack/test_models/test_slack_user.py +9 -9
  198. firefighter_tests/test_slack/test_slack_utils.py +6 -6
  199. firefighter_tests/test_slack/views/modals/test_close.py +7 -7
  200. firefighter_tests/test_slack/views/modals/test_open.py +7 -7
  201. firefighter_tests/test_slack/views/modals/test_send_sos.py +2 -2
  202. firefighter_tests/test_slack/views/modals/test_status.py +2 -2
  203. firefighter_tests/test_slack/views/modals/test_update_status.py +4 -4
  204. manage.py +1 -0
  205. package-lock.json +6442 -0
  206. package.json +49 -0
  207. scripts/gen_credits.py +165 -0
  208. scripts/hatch_build.py +15 -0
  209. firefighter/raid/signals/update_qualifiers_rotation.py +0 -97
  210. firefighter/raid/tasks/daily_qualifier.py +0 -67
  211. firefighter/raid/tasks/weekly_qualifier.py +0 -63
  212. {firefighter_incident-0.0.1rc2.dist-info → firefighter_incident-0.0.3.dist-info}/entry_points.txt +0 -0
  213. {firefighter_incident-0.0.1rc2.dist-info → firefighter_incident-0.0.3.dist-info}/licenses/LICENSE +0 -0
@@ -4,13 +4,8 @@
4
4
  x-data="{ open: false }"
5
5
  {% if autoplay == True %}x-init="$nextTick(() => { open = true })"{% endif %} >
6
6
  <div x-ref="modal1_button"
7
- @click="open = true">
7
+ @click="open = true">
8
8
  {% slot "modal_enabler" %}
9
- <button
10
- type="button"
11
- class="w-full bg-primary px-4 py-2 border border-transparent rounded-md flex items-center justify-center text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:w-auto sm:inline-flex">
12
- Open Modal
13
- </button>
14
9
  {% endslot %}
15
10
  </div>
16
11
  <div
@@ -24,27 +19,27 @@
24
19
  @keydown.escape="open = false"
25
20
  @click.away="open = false"
26
21
  style="display: none;"
27
- class="z-50 fixed top-0 left-0 w-full h-screen flex justify-center items-center" >
22
+ class="z-[100] fixed top-0 left-0 w-full h-screen flex justify-center items-center" >
28
23
  <div aria-hidden="true"
29
- class="absolute top-0 left-0 w-full h-screen bg-black transition duration-300 z-[90]"
30
- :class="{ 'opacity-60': open, 'opacity-0': !open }"
31
- x-show="open"
32
- style="display: none;"
33
- x-transition:leave="delay-150"></div>
24
+ class="absolute top-0 left-0 w-full h-screen bg-black transition duration-300"
25
+ :class="{ 'opacity-60': open, 'opacity-0': !open }"
26
+ x-show="open"
27
+ style="display: none;"
28
+ x-transition:leave="delay-150"></div>
34
29
  <div data-modal-document
35
- @click.stop=""
36
- @keydown.escape="open = false"
37
- x-show="open"
38
- x-trap="open"
39
- x-trap.noscroll="open"
40
- x-trap.inert="open"
41
- x-transition:enter="transition ease-out duration-300"
42
- x-transition:enter-start="transform scale-50 opacity-0"
43
- x-transition:enter-end="transform scale-100 opacity-100"
44
- x-transition:leave="transition ease-out duration-200"
45
- x-transition:leave-start="transform scale-100 opacity-100"
46
- x-transition:leave-end="transform scale-50 opacity-0"
47
- class="flex flex-col rounded-lg shadow-lg overflow-hidden bg-white dark:bg-neutral-900 dark:text-neutral-100 m-w-4/5 xl:m-w-3/5 min-h-4/5 z-[100]" style="max-height: 95vh;">
30
+ @click.stop=""
31
+ @keydown.escape="open = false"
32
+ x-show="open"
33
+ x-trap="open"
34
+ x-trap.noscroll="open"
35
+ x-trap.inert="open"
36
+ x-transition:enter="transition ease-out duration-300"
37
+ x-transition:enter-start="transform scale-50 opacity-0"
38
+ x-transition:enter-end="transform scale-100 opacity-100"
39
+ x-transition:leave="transition ease-out duration-200"
40
+ x-transition:leave-start="transform scale-100 opacity-100"
41
+ x-transition:leave-end="transform scale-50 opacity-0"
42
+ class="flex flex-col rounded-lg shadow-lg overflow-hidden bg-white dark:bg-neutral-900 dark:text-neutral-100 m-w-4/5 xl:m-w-3/5 min-h-4/5 z-[100]" style="max-height: 95vh;">
48
43
  <div class="p-6 border-b border-neutral-300 dark:border-neutral-700 flex justify-between">
49
44
  <h1 id="modal1_label" class="font-semibold" x-ref="modal1_label"> {% slot "modal_header" %} {% if title %}{{ title }}{% endif %}{% endslot %}</h1>
50
45
 
@@ -1,20 +1,35 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from typing import Any
4
+ from typing import Any, NotRequired, Required, TypedDict, Unpack
5
5
 
6
- from django_components import component
6
+ from django.utils.safestring import SafeString
7
+ from django_components import EmptyTuple, component
8
+ from django_components.slots import SlotFunc
7
9
 
8
10
  logger = logging.getLogger(__name__)
9
11
 
12
+ SlotContent = str | SafeString | SlotFunc[Any]
13
+
14
+
15
+ class Kwargs(TypedDict, total=False):
16
+ autoplay: Required[bool]
17
+ header: NotRequired[bool]
18
+
19
+
20
+ class Data(TypedDict):
21
+ autoplay: bool
22
+
23
+
24
+ class Slots(TypedDict):
25
+ modal_header: NotRequired[SlotContent]
26
+ modal_content: NotRequired[SlotContent]
27
+ modal_enabler: NotRequired[SlotContent]
28
+
10
29
 
11
30
  @component.register("modal")
12
- class Modal(component.Component):
31
+ class Modal(component.Component[EmptyTuple, Kwargs, Data, Any]): # type: ignore[type-var]
13
32
  template_name = "modal/modal.html"
14
33
 
15
- def get_context_data(
16
- self, *args: Any, autoplay: bool = False, **kwargs: Any
17
- ) -> dict[str, bool]:
18
- return {
19
- "autoplay": autoplay,
20
- }
34
+ def get_context_data(self, *args: Any, **kwargs: Unpack[Kwargs]) -> Data:
35
+ return Kwargs(autoplay=kwargs["autoplay"])
@@ -79,6 +79,8 @@ class PostMortemManager(models.Manager["PostMortem"]):
79
79
  class ConfluencePage(models.Model):
80
80
  """Represents a Confluence page."""
81
81
 
82
+ objects: ClassVar[models.Manager[ConfluencePage]]
83
+
82
84
  id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
83
85
 
84
86
  name = models.CharField(max_length=255)
@@ -149,7 +149,7 @@ class ConfluenceService:
149
149
  logger.info("Confluence OnCall page is up to date, and was not updated.")
150
150
  return False
151
151
 
152
- def create_postmortem(self, title: str) -> None | ConfluencePage:
152
+ def create_postmortem(self, title: str) -> ConfluencePage | None:
153
153
  """Create a PostMortem page.
154
154
 
155
155
  Args:
@@ -258,7 +258,7 @@ class ConfluenceService:
258
258
  position: Literal["before", "after", "append"] = "append",
259
259
  *,
260
260
  dry_run: bool = False,
261
- ) -> None | ConfluencePage:
261
+ ) -> ConfluencePage | None:
262
262
  """Args:
263
263
  page_id (ConfluencePageId): ID of the page to move.
264
264
  target_page_id (ConfluencePageId): ID of the parent page to move the page in relation to.
@@ -325,7 +325,7 @@ if settings.CONFLUENCE_MOCK_CREATE_POSTMORTEM:
325
325
  "status": "current",
326
326
  "title": title,
327
327
  "_links": {
328
- "editui": "/pages/resumedraft.action?draftId={fake_id}",
328
+ "editui": f"/pages/resumedraft.action?draftId={fake_id}",
329
329
  "webui": f"/spaces/{settings.CONFLUENCE_POSTMORTEM_SPACE}/pages/{fake_id}",
330
330
  "context": "/wiki",
331
331
  "self": f"{settings.CONFLUENCE_URL}/content/{fake_id}",
@@ -1,9 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
+ import operator
4
5
 
5
6
  from celery import shared_task
6
- from django.utils.timezone import datetime, get_current_timezone, timedelta # type: ignore[attr-defined]
7
+ from django.utils.timezone import ( # type: ignore[attr-defined]
8
+ datetime,
9
+ get_current_timezone,
10
+ timedelta,
11
+ )
7
12
 
8
13
  from firefighter.confluence.service import confluence_service
9
14
  from firefighter.confluence.utils import (
@@ -206,7 +211,7 @@ def sort_postmortems_in_bins(
206
211
  if date_ and fmt:
207
212
  quarter_children.append((page_id, date_, page))
208
213
 
209
- sorted_children = sorted(quarter_children, key=lambda x: x[1], reverse=True)
214
+ sorted_children = sorted(quarter_children, key=operator.itemgetter(1), reverse=True)
210
215
  # Check if already sorted
211
216
  if sorted_children == quarter_children:
212
217
  logger.debug(f"Quarter {quarter} is already sorted. Skipping")
@@ -56,7 +56,7 @@ def sync_postmortems() -> None:
56
56
  if not Incident.objects.filter(id=incident_id).exists():
57
57
  pm_missing_incident.append(data["name"])
58
58
  continue
59
- data_editable = cast(dict[str, str], data)
59
+ data_editable = cast("dict[str, str]", data)
60
60
  try:
61
61
  PostMortem.objects.update_or_create(
62
62
  page_id=int(data_editable.pop("page_id")),
@@ -47,7 +47,7 @@ def sync_runbooks() -> None:
47
47
  all_fetched_ids.add(page_id)
48
48
  data["name"] = data["name"].removesuffix("[RUNBOOK]").strip()
49
49
 
50
- data_editable = cast(dict[str, str], data)
50
+ data_editable = cast("dict[str, str]", data)
51
51
  data_editable["title"] = (
52
52
  data_editable["name"].removesuffix("[RUNBOOK]").strip()
53
53
  )
@@ -77,11 +77,11 @@ def sync_runbooks() -> None:
77
77
  return
78
78
  if len(missing_runbooks) > 4:
79
79
  logger.warning(
80
- f"Too many runbooks not found. FireFighter will not delete them automatically, as it might be an API or implementation error. List: {[f'{x.page_id }/ {x.name}' for x in missing_runbooks]}"
80
+ f"Too many runbooks not found. FireFighter will not delete them automatically, as it might be an API or implementation error. List: {[f'{x.page_id}/ {x.name}' for x in missing_runbooks]}"
81
81
  )
82
82
  return
83
83
  logger.warning(
84
- f"Missing runbooks that will be deleted: {[f'{x.page_id }/ {x.name}' for x in missing_runbooks]}"
84
+ f"Missing runbooks that will be deleted: {[f'{x.page_id}/ {x.name}' for x in missing_runbooks]}"
85
85
  )
86
86
  # Delete all runbooks that are not in the folders anymore
87
87
  missing_runbooks.delete()
@@ -1,6 +1,6 @@
1
- <h2><a href="{{oncall_page_link}}">On-call</a>:</h2>
1
+ <h2><a href="{{ oncall_page_link }}">On-call</a>:</h2>
2
2
  <ul>
3
3
  {% for team_name, user in users %}
4
- <li>{{ team_name | upper }} {% if user.slack_user %}<a href="{{ user.slack_user.url}}">{% endif %}{{ user.full_name }}{% if user.slack_user %}</a>{% endif %}{% if user.pagerduty_user.phone_number %}: <a href="tel:+{{ user.pagerduty_user.phone_number }}">+{{ user.pagerduty_user.phone_number }}</a>{% endif %}</li>
5
- {% endfor -%}
4
+ <li>{{ team_name|upper }} {% if user.slack_user %}<a href="{{ user.slack_user.url }}">{% endif %}{{ user.full_name }}{% if user.slack_user %}</a>{% endif %}{% if user.pagerduty_user.phone_number %}: <a href="tel:+{{ user.pagerduty_user.phone_number }}">+{{ user.pagerduty_user.phone_number }}</a>{% endif %}</li>
5
+ {% endfor - %}
6
6
  </ul>
@@ -1,20 +1,19 @@
1
1
  {% extends '../layouts/view_filters.html' %}
2
2
 
3
3
  {% block page_title %}
4
- Runbooks <div role="status" class="progress htmx-indicator inline">
4
+ Runbooks <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"/>
8
8
  </svg>
9
9
  <span class="sr-only">Loading...</span>
10
10
  </div>
11
- {% endblock%}
11
+ {% endblock page_title %}
12
12
 
13
13
  {% block page_actions %}
14
14
  {{ block.super }}
15
- {% endblock%}
16
-
15
+ {% endblock page_actions %}
17
16
 
18
17
  {% block page_content %}
19
18
  {% include "./../layouts/partials/partial_table_list_paginated.html" %}
20
- {% endblock%}
19
+ {% endblock page_content %}
@@ -3,7 +3,10 @@ from __future__ import annotations
3
3
  import re
4
4
  from typing import TypeAlias, TypedDict
5
5
 
6
- from django.utils.timezone import datetime, get_current_timezone # type: ignore[attr-defined]
6
+ from django.utils.timezone import ( # type: ignore[attr-defined]
7
+ datetime,
8
+ get_current_timezone,
9
+ )
7
10
 
8
11
  from firefighter.incidents.views.date_utils import get_quarter_from_week
9
12
 
@@ -7,13 +7,14 @@ from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast
7
7
 
8
8
  import markdown as md
9
9
  import nh3
10
- from django import template
11
10
  from django.template.defaulttags import register as register_base
12
11
 
13
12
  if TYPE_CHECKING:
14
13
  from collections.abc import Callable
15
14
 
16
- register_global: template.Library = cast(template.Library, register_base)
15
+ from django import template
16
+
17
+ register_global: template.Library = cast("template.Library", register_base)
17
18
  V = TypeVar("V")
18
19
 
19
20
 
@@ -6,9 +6,9 @@ from typing import Any
6
6
  import httpx
7
7
  from django.conf import settings
8
8
 
9
- FF_HTTP_CLIENT_ADDITIONAL_HEADERS: dict[
10
- str, Any
11
- ] | None = settings.FF_HTTP_CLIENT_ADDITIONAL_HEADERS
9
+ FF_HTTP_CLIENT_ADDITIONAL_HEADERS: dict[str, Any] | None = (
10
+ settings.FF_HTTP_CLIENT_ADDITIONAL_HEADERS
11
+ )
12
12
 
13
13
 
14
14
  class HttpClient:
@@ -28,12 +28,10 @@ class HttpClient:
28
28
  self._client = httpx.Client(**(client_kwargs or {}))
29
29
  self._client.timeout = httpx.Timeout(15, read=20)
30
30
  if FF_HTTP_CLIENT_ADDITIONAL_HEADERS:
31
- self._client.headers = httpx.Headers(
32
- {
33
- **self._client.headers,
34
- **FF_HTTP_CLIENT_ADDITIONAL_HEADERS,
35
- }
36
- )
31
+ self._client.headers = httpx.Headers({
32
+ **self._client.headers,
33
+ **FF_HTTP_CLIENT_ADDITIONAL_HEADERS,
34
+ })
37
35
 
38
36
  def call(self, method: str, url: str, **kwargs: Any) -> httpx.Response:
39
37
  res: httpx.Response = getattr(self._client, method)(url, **kwargs)
@@ -31,5 +31,5 @@ class Command(BaseCommand):
31
31
  self.stdout.write(self.style.SUCCESS(f"Successfully ran task {task_name}"))
32
32
  else:
33
33
  sep = "\n - "
34
- err_msg = f'Task "{task_name}" does not exist. Available tasks are:\n - { sep.join(sorted(tasks))}'
34
+ err_msg = f'Task "{task_name}" does not exist. Available tasks are:\n - {sep.join(sorted(tasks))}'
35
35
  raise CommandError(err_msg)
@@ -10,6 +10,7 @@
10
10
  ```
11
11
 
12
12
  """
13
+
13
14
  # ruff: noqa: E402, F403
14
15
  # pylint: disable=wrong-import-position
15
16
  # isort: off
@@ -54,6 +54,7 @@ INSTALLED_APPS = [
54
54
  "django_filters",
55
55
  "taggit",
56
56
  "django_tables2",
57
+ "import_export",
57
58
  # SSO Auth
58
59
  "oauth2_authcodeflow",
59
60
  # Celery integration
@@ -87,6 +88,11 @@ WSGI_APPLICATION = "firefighter.firefighter.wsgi.application"
87
88
  # Database
88
89
  # https://docs.djangoproject.com/en/4.2/ref/settings/#databases
89
90
 
91
+ db_schema = config("POSTGRES_SCHEMA", default="")
92
+ db_options = {"options": "-c statement_timeout=30000"}
93
+ if db_schema:
94
+ db_options["options"] += f" -c search_path={db_schema}"
95
+
90
96
  DATABASES: dict[str, dict[str, Any]] = {
91
97
  "default": {
92
98
  "ENGINE": "django.db.backends.postgresql",
@@ -95,9 +101,7 @@ DATABASES: dict[str, dict[str, Any]] = {
95
101
  "PASSWORD": config("POSTGRES_PASSWORD"),
96
102
  "HOST": config("POSTGRES_HOST"),
97
103
  "PORT": config("POSTGRES_PORT"),
98
- "OPTIONS": {
99
- "options": "-c statement_timeout=30000",
100
- },
104
+ "OPTIONS": db_options,
101
105
  }
102
106
  }
103
107
 
@@ -269,6 +273,7 @@ PLAUSIBLE_DOMAIN: str = config("PLAUSIBLE_DOMAIN", "")
269
273
  # Components
270
274
  COMPONENTS = {
271
275
  "autodiscover": False,
276
+ "context_behavior": "django", # Like before django-components 0.67
272
277
  "libraries": [
273
278
  "firefighter.components.avatar.avatar",
274
279
  "firefighter.components.card.card",
@@ -27,7 +27,7 @@ class AccessLogFilter(Filter):
27
27
  return False
28
28
  if raw_uri.startswith("/static/") and record.levelno <= 20:
29
29
  return False
30
- if raw_uri == "/favicon.ico" and record.levelno <= 30:
30
+ if raw_uri == "/favicon.ico" and record.levelno <= 30: # noqa: SIM103
31
31
  return False
32
32
  return True
33
33
 
@@ -15,8 +15,8 @@ if ENABLE_RAID:
15
15
  RAID_JIRA_PROJECT_KEY: str = config("RAID_JIRA_PROJECT_KEY")
16
16
  "The Jira project key to use for creating issues, e.g. 'INC'"
17
17
 
18
- RAID_QUALIFIER_URL: str = config("RAID_QUALIFIER_URL")
19
- "Link to the board with issues to qualify"
20
-
21
18
  RAID_JIRA_USER_IDS: dict[str, str] = {}
22
19
  "Mapping of domain to default Jira user ID"
20
+
21
+ RAID_TOOLBOX_URL: str = config("RAID_TOOLBOX_URL")
22
+ "Toolbox URL"
@@ -143,8 +143,6 @@ EXTRA_CHECKS = {
143
143
  "no-unique-together",
144
144
  # Require non empty `upload_to` argument:
145
145
  "field-file-upload-to",
146
- # Use the indexes option instead:
147
- "no-index-together",
148
146
  # Each model must be registered in admin:
149
147
  # "model-admin",
150
148
  # FileField/ImageField must have non-empty `upload_to` argument:
@@ -182,7 +180,7 @@ EXTRA_CHECKS = {
182
180
  DATABASES["default"]["CONN_MAX_AGE"] = 0
183
181
 
184
182
  # Force Debug on Django templating system
185
- TEMPLATES[0]["OPTIONS"] = TEMPLATES[0]["OPTIONS"] | {"debug": True} # type: ignore[operator]
183
+ TEMPLATES[0]["OPTIONS"] |= {"debug": True} # type: ignore[operator]
186
184
 
187
185
 
188
186
  FF_DEBUG_ERROR_PAGES = config("FF_DEBUG_ERROR_PAGES", default=True, cast=bool)
@@ -2,6 +2,7 @@
2
2
 
3
3
  This file is required and if `ENV=dev` these values are not used.
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
 
7
8
  from pathlib import Path
@@ -34,7 +34,7 @@ def link_auth_user(user: User, claim: dict[str, str | list[str]]) -> None:
34
34
  for group_name in group_names:
35
35
  try:
36
36
  group = Group.objects.get(name=group_name)
37
- group.user_set.add( # type: ignore[attr-defined]
37
+ group.user_set.add(
38
38
  user
39
39
  ) # pyright: ignore[reportGeneralTypeIssues]
40
40
  except Group.DoesNotExist:
@@ -2,7 +2,8 @@
2
2
  {% load static %}
3
3
  {% block extrahead %}{{ block.super }}
4
4
  <link rel="icon" href="{% static 'img/favicon/favicon.ico' %}" sizes="48x48" />
5
- {% endblock%}
5
+ {% endblock extrahead %}
6
+
6
7
  {% block extrastyle %}{{ block.super }}
7
8
  <style>
8
9
  body {
@@ -103,4 +104,4 @@
103
104
  }
104
105
  }
105
106
  </style>
106
- {% endblock %}
107
+ {% endblock extrastyle %}
@@ -2,7 +2,7 @@
2
2
 
3
3
  {% block content %}
4
4
  <div style="padding:2rem; text-align: center;">
5
- <a class="button" style="padding: 15px 20px; font-size:16px" href="{% url "oidc_authentication"%}?next={% if request.GET.next is not None %}{{ request.GET.next | default:"/" | urlencode}}{% else %}{{"/" | urlencode}}{% endif %}&fail=/admin/login/"/>Log in with SSO<a/>
5
+ <a class="button" style="padding: 15px 20px; font-size:16px" href="{% url "oidc_authentication" %}?next={% if request.GET.next is not None %}{{ request.GET.next|default:"/"|urlencode }}{% else %}{{ "/"|urlencode }}{% endif %}&fail=/admin/login/"/>Log in with SSO<a/>
6
6
  </div>
7
7
  {% if request.GET.error %}
8
8
  <p class="errornote">
@@ -11,4 +11,4 @@
11
11
  {% endif %}
12
12
 
13
13
  {{ block.super }}
14
- {% endblock %}
14
+ {% endblock content %}
@@ -1,5 +1,5 @@
1
1
  {% extends "admin/base_site.html" %}
2
- {% load i18n l10n admin_urls static %}
2
+ {% load admin_urls i18n l10n static %}
3
3
 
4
4
  {% comment %} TODO Proper form and HTML. {% endcomment %}
5
5
 
@@ -7,7 +7,7 @@
7
7
  {{ block.super }}
8
8
  {{ media }}
9
9
  <script src="{% static 'admin/js/cancel.js' %}" async></script>
10
- {% endblock %}
10
+ {% endblock extrahead %}
11
11
 
12
12
  {% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} send-message-conversation send-message-selected-conversation{% endblock %}
13
13
 
@@ -18,7 +18,7 @@
18
18
  &rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
19
19
  &rsaquo; {% translate 'Send messages on conversations' %}
20
20
  </div>
21
- {% endblock %}
21
+ {% endblock breadcrumbs %}
22
22
 
23
23
  {% block content %}
24
24
  <h2>Targeted conversations</h2>
@@ -42,4 +42,4 @@
42
42
  <a href="#" class="button cancel-link">{% translate "Cancel, take me back" %}</a>
43
43
  </div>
44
44
  </form>
45
- {% endblock %}
45
+ {% endblock content %}
@@ -20,6 +20,7 @@ The `urlpatterns` list routes URLs to views. For more information please see:
20
20
  1. Import the include() function: `from django.urls import include, path`
21
21
  2. Add a URL to urlpatterns: `path('blog/', include('blog.urls'))`
22
22
  """
23
+
23
24
  from __future__ import annotations
24
25
 
25
26
  import sys
@@ -88,14 +89,12 @@ if settings.ENV == "dev":
88
89
  )
89
90
 
90
91
  if settings.FF_DEBUG_ERROR_PAGES:
91
- firefighter_urlpatterns[0].extend(
92
- (
93
- path("err/403/", views.permission_denied_view),
94
- path("err/404/", views.not_found_view),
95
- path("err/400/", views.bad_request_view),
96
- path("err/500/", views.server_error_view),
97
- )
98
- )
92
+ firefighter_urlpatterns[0].extend((
93
+ path("err/403/", views.permission_denied_view),
94
+ path("err/404/", views.not_found_view),
95
+ path("err/400/", views.bad_request_view),
96
+ path("err/500/", views.server_error_view),
97
+ ))
99
98
 
100
99
  if apps.is_installed("firefighter.slack") and (
101
100
  "runserver" in sys.argv
@@ -76,9 +76,7 @@ def is_during_office_hours(dt: datetime) -> bool:
76
76
  Args:
77
77
  dt (datetime): datetime with TZ info.
78
78
  """
79
- if (9 <= dt.hour <= 17) and (dt.weekday() < 5):
80
- return True
81
- return False
79
+ return (9 <= dt.hour <= 17) and (dt.weekday() < 5)
82
80
 
83
81
 
84
82
  # Typing for custom HttpRequest classes
@@ -5,6 +5,7 @@ It exposes the WSGI callable as a module-level variable named ``application``.
5
5
  For more information on this file, see
6
6
  https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
7
7
  """
8
+
8
9
  from __future__ import annotations
9
10
 
10
11
  # We need to be able to shadow virtualenv things with our own code
@@ -17,6 +18,6 @@ import os
17
18
  os.environ.setdefault("DJANGO_SETTINGS_MODULE", "firefighter.firefighter.settings")
18
19
  # pylint: disable=wrong-import-position
19
20
  # noinspection PyPep8
20
- from django.core.wsgi import get_wsgi_application # noqa: E402
21
+ from django.core.wsgi import get_wsgi_application
21
22
 
22
23
  application = get_wsgi_application()
@@ -87,7 +87,9 @@ class IncidentMembershipInline(admin.StackedInline[IncidentMembership, Incident]
87
87
  verbose_name = _("Incident Member")
88
88
 
89
89
  def has_change_permission(
90
- self, request: HttpRequest, obj: Any | None = None # noqa: ARG002
90
+ self,
91
+ request: HttpRequest, # noqa: ARG002
92
+ obj: Any | None = None, # noqa: ARG002
91
93
  ) -> bool:
92
94
  return False
93
95
 
@@ -293,7 +295,7 @@ class IncidentAdmin(admin.ModelAdmin[Incident]):
293
295
  )
294
296
  def send_message(
295
297
  self, request: HttpRequest, queryset: QuerySet[Incident]
296
- ) -> None | TemplateResponse:
298
+ ) -> TemplateResponse | None:
297
299
  """Action to send a message in selected channels.
298
300
  This action first displays a confirmation page to enter the message.
299
301
  Next, it sends the message on all selected objects and redirects back to the change list (other fn).
@@ -547,8 +549,8 @@ class GroupAdmin(admin.ModelAdmin[Group]):
547
549
 
548
550
 
549
551
  @admin.register(User)
550
- class UserAdmin(BaseUserAdmin):
551
- model = User # type: ignore[assignment]
552
+ class UserAdmin(BaseUserAdmin): # type: ignore[type-arg]
553
+ model = User
552
554
  list_max_show_all = 500
553
555
  inlines = user_inlines
554
556
  readonly_fields = [
@@ -562,7 +564,7 @@ class UserAdmin(BaseUserAdmin):
562
564
  list_filter = ("is_staff", "is_superuser", "is_active", "groups", "bot")
563
565
 
564
566
  def __init__(self, model: type[User], admin_site: AdminSite) -> None:
565
- super().__init__(model, admin_site) # type: ignore[arg-type]
567
+ super().__init__(model, admin_site)
566
568
  self.fieldsets[2][1]["fields"] = ("bot", *self.fieldsets[2][1]["fields"]) # type: ignore
567
569
  self.fieldsets[1][1]["fields"] = (*self.fieldsets[1][1]["fields"], "avatar") # type: ignore
568
570
 
@@ -579,9 +581,11 @@ class UserAdmin(BaseUserAdmin):
579
581
  return obj.incidents_created_by.count()
580
582
 
581
583
  def get_fieldsets(
582
- self, request: HttpRequest, obj: User | None = None # type: ignore[override]
584
+ self,
585
+ request: HttpRequest,
586
+ obj: User | None = None,
583
587
  ) -> _FieldsetSpec:
584
- fieldsets = list(super().get_fieldsets(request, obj)) # type: ignore[arg-type]
588
+ fieldsets = list(super().get_fieldsets(request, obj))
585
589
  fieldsets.append(
586
590
  (
587
591
  _("User statistics"),