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.
- firefighter/_version.py +9 -4
- firefighter/api/admin.py +31 -12
- firefighter/api/migrations/0003_alter_apitokenproxy_options.py +30 -0
- firefighter/api/models.py +2 -2
- firefighter/api/renderer.py +1 -1
- firefighter/api/serializers.py +6 -4
- firefighter/api/urls.py +13 -19
- firefighter/components/avatar/avatar.py +24 -7
- firefighter/components/card/card.py +9 -4
- firefighter/components/export_button/export_button.html +2 -2
- firefighter/components/export_button/export_button.py +26 -7
- firefighter/components/form/form.html +1 -1
- firefighter/components/form/form.py +15 -12
- firefighter/components/form_field/form_field.html +4 -4
- firefighter/components/form_field/form_field.py +20 -12
- firefighter/components/messages/messages.html +39 -49
- firefighter/components/messages/messages.py +10 -5
- firefighter/components/modal/modal.html +20 -25
- firefighter/components/modal/modal.py +24 -9
- firefighter/confluence/models.py +2 -0
- firefighter/confluence/service.py +3 -3
- firefighter/confluence/tasks/archive_postmortems.py +7 -2
- firefighter/confluence/tasks/sync_postmortems.py +1 -1
- firefighter/confluence/tasks/sync_runbooks.py +3 -3
- firefighter/confluence/templates/oncall_team.xml +3 -3
- firefighter/confluence/templates/pages/runbook_list.html +4 -5
- firefighter/confluence/utils.py +4 -1
- firefighter/firefighter/filters.py +3 -2
- firefighter/firefighter/http_client.py +7 -9
- firefighter/firefighter/management/commands/task.py +1 -1
- firefighter/firefighter/settings/__init__.py +1 -0
- firefighter/firefighter/settings/components/common.py +8 -3
- firefighter/firefighter/settings/components/logging.py +1 -1
- firefighter/firefighter/settings/components/raid.py +3 -3
- firefighter/firefighter/settings/environments/dev.py +1 -3
- firefighter/firefighter/settings/environments/prod.py +1 -0
- firefighter/firefighter/sso.py +1 -1
- firefighter/firefighter/templates/admin/base.html +3 -2
- firefighter/firefighter/templates/admin/login.html +2 -2
- firefighter/firefighter/templates/admin/send_message_conversation.html +4 -4
- firefighter/firefighter/urls.py +7 -8
- firefighter/firefighter/utils.py +1 -3
- firefighter/firefighter/wsgi.py +2 -1
- firefighter/incidents/admin.py +11 -7
- firefighter/incidents/factories.py +33 -32
- firefighter/incidents/forms/edit.py +33 -0
- firefighter/incidents/forms/select_impact.py +4 -11
- firefighter/incidents/forms/update_key_events.py +1 -1
- firefighter/incidents/forms/update_roles.py +1 -1
- firefighter/incidents/forms/utils.py +3 -3
- firefighter/incidents/menus.py +5 -4
- firefighter/incidents/migrations/0002_alter_severity_name_alter_user_password_featureteam.py +35 -0
- firefighter/incidents/migrations/0003_delete_featureteam.py +16 -0
- firefighter/incidents/migrations/0004_incidentupdate_environment.py +27 -0
- firefighter/incidents/migrations/0005_enable_from_p1_to_p5_priority.py +30 -0
- firefighter/incidents/migrations/0006_update_group_names.py +102 -0
- firefighter/incidents/migrations/0007_update_component_name.py +148 -0
- firefighter/incidents/migrations/0008_impact_level.py +273 -0
- firefighter/incidents/models/component.py +8 -11
- firefighter/incidents/models/group.py +1 -0
- firefighter/incidents/models/impact.py +42 -12
- firefighter/incidents/models/incident.py +14 -11
- firefighter/incidents/models/incident_cost.py +2 -2
- firefighter/incidents/models/incident_membership.py +5 -5
- firefighter/incidents/models/incident_update.py +14 -5
- firefighter/incidents/models/metric_type.py +2 -2
- firefighter/incidents/signals.py +0 -5
- firefighter/incidents/static/css/main.min.css +1 -1
- firefighter/incidents/static/css/tailwind.css +18 -34
- firefighter/incidents/static/js/main.min.js +12 -12
- firefighter/incidents/tables.py +1 -5
- firefighter/incidents/tasks/updateoncall.py +1 -3
- firefighter/incidents/templates/incidents/errors/base.html +2 -1
- firefighter/incidents/templates/incidents/filter.html +33 -33
- firefighter/incidents/templates/incidents/table/priority_column.html +1 -1
- firefighter/incidents/templates/incidents/table.html +2 -3
- firefighter/incidents/templates/incidents/widgets/form_container.html +60 -61
- firefighter/incidents/templates/incidents/widgets/grouped_checkbox_nested.html +52 -52
- firefighter/incidents/templates/layouts/index.html +3 -3
- firefighter/incidents/templates/layouts/partials/created_at_help.html +2 -3
- firefighter/incidents/templates/layouts/partials/environment_pill.html +6 -6
- firefighter/incidents/templates/layouts/partials/footer.html +4 -7
- firefighter/incidents/templates/layouts/partials/header.html +41 -31
- firefighter/incidents/templates/layouts/partials/incident_card.html +6 -6
- firefighter/incidents/templates/layouts/partials/incident_metrics.html +1 -2
- firefighter/incidents/templates/layouts/partials/incident_timeline.html +35 -6
- firefighter/incidents/templates/layouts/partials/incident_update_key_events_view.html +3 -3
- firefighter/incidents/templates/layouts/partials/incident_update_key_events_view_modal.html +5 -7
- firefighter/incidents/templates/layouts/partials/partial_table_list_paginated.html +8 -9
- firefighter/incidents/templates/layouts/partials/priority_pill.html +1 -1
- firefighter/incidents/templates/layouts/partials/status_pill.html +15 -15
- firefighter/incidents/templates/layouts/partials/table.html +3 -3
- firefighter/incidents/templates/layouts/partials/user_card.html +3 -4
- firefighter/incidents/templates/layouts/partials/user_tooltip.html +1 -1
- firefighter/incidents/templates/layouts/view_filters.html +9 -9
- firefighter/incidents/templates/pages/component_detail.html +9 -9
- firefighter/incidents/templates/pages/component_list.html +5 -7
- firefighter/incidents/templates/pages/dashboard.html +4 -4
- firefighter/incidents/templates/pages/docs_metrics.html +43 -41
- firefighter/incidents/templates/pages/incident_create.html +9 -10
- firefighter/incidents/templates/pages/incident_detail.html +52 -46
- firefighter/incidents/templates/pages/incident_list.html +7 -7
- firefighter/incidents/templates/pages/incident_role_types_detail.html +3 -5
- firefighter/incidents/templates/pages/incident_role_types_list.html +3 -3
- firefighter/incidents/templates/pages/incident_statistics.html +10 -10
- firefighter/incidents/templates/pages/incident_statistics_partial.html +7 -11
- firefighter/incidents/templates/pages/incident_update_key_events_form.html +1 -1
- firefighter/incidents/templates/pages/user_detail.html +15 -15
- firefighter/incidents/views/docs/role_types.py +2 -1
- firefighter/incidents/views/errors.py +15 -16
- firefighter/incidents/views/reports.py +8 -8
- firefighter/incidents/views/users/details.py +1 -4
- firefighter/jira_app/client.py +3 -3
- firefighter/jira_app/models.py +2 -2
- firefighter/logging/custom_json_formatter.py +2 -2
- firefighter/pagerduty/models.py +9 -1
- firefighter/pagerduty/tasks/__init__.py +1 -0
- firefighter/pagerduty/tasks/trigger_oncall.py +7 -9
- firefighter/pagerduty/templates/pages/oncall_list.html +7 -7
- firefighter/pagerduty/templates/pages/oncall_trigger.html +1 -1
- firefighter/pagerduty/templates/partials/trigger_oncall_form_view.html +3 -3
- firefighter/pagerduty/templates/partials/trigger_oncall_form_view_modal.html +5 -7
- firefighter/raid/admin.py +13 -87
- firefighter/raid/apps.py +0 -1
- firefighter/raid/client.py +12 -7
- firefighter/raid/forms.py +5 -17
- firefighter/raid/messages.py +6 -90
- firefighter/raid/migrations/0002_featureteam_remove_qualifierrotation_jira_user_and_more.py +36 -0
- firefighter/raid/models.py +28 -46
- firefighter/raid/resources.py +15 -0
- firefighter/raid/service.py +5 -28
- firefighter/raid/signals/incident_created.py +2 -13
- firefighter/raid/tasks/__init__.py +0 -3
- firefighter/raid/views/open_normal.py +1 -1
- firefighter/slack/admin.py +39 -3
- firefighter/slack/factories.py +32 -32
- firefighter/slack/messages/slack_messages.py +47 -33
- firefighter/slack/migrations/0002_usergroup_tag.py +22 -0
- firefighter/slack/migrations/0003_alter_usergroup_tag.py +22 -0
- firefighter/slack/models/conversation.py +3 -3
- firefighter/slack/models/incident_channel.py +2 -2
- firefighter/slack/models/message.py +4 -4
- firefighter/slack/models/sos.py +2 -2
- firefighter/slack/models/user_group.py +6 -0
- firefighter/slack/rules.py +3 -4
- firefighter/slack/signals/create_incident_conversation.py +2 -1
- firefighter/slack/signals/get_users.py +29 -3
- firefighter/slack/signals/incident_updated.py +2 -2
- firefighter/slack/slack_app.py +18 -2
- firefighter/slack/slack_incident_context.py +8 -8
- firefighter/slack/tasks/fetch_conversations_members.py +1 -1
- firefighter/slack/tasks/send_reminders.py +1 -1
- firefighter/slack/tasks/update_usergroups_members.py +1 -1
- firefighter/slack/views/events/commands.py +3 -0
- firefighter/slack/views/events/home.py +22 -20
- firefighter/slack/views/modals/__init__.py +2 -0
- firefighter/slack/views/modals/base_modal/base.py +2 -2
- firefighter/slack/views/modals/base_modal/form_utils.py +6 -10
- firefighter/slack/views/modals/base_modal/mixins.py +1 -2
- firefighter/slack/views/modals/close.py +15 -13
- firefighter/slack/views/modals/downgrade_workflow.py +13 -17
- firefighter/slack/views/modals/edit.py +117 -0
- firefighter/slack/views/modals/key_event_message.py +9 -9
- firefighter/slack/views/modals/open.py +21 -21
- firefighter/slack/views/modals/opening/check_current_incidents.py +3 -3
- firefighter/slack/views/modals/opening/details/critical.py +7 -5
- firefighter/slack/views/modals/opening/select_impact.py +11 -14
- firefighter/slack/views/modals/opening/set_details.py +4 -2
- firefighter/slack/views/modals/opening/types.py +1 -1
- firefighter/slack/views/modals/postmortem.py +16 -20
- firefighter/slack/views/modals/select.py +1 -1
- firefighter/slack/views/modals/status.py +17 -21
- firefighter/slack/views/modals/trigger_oncall.py +19 -21
- firefighter/slack/views/modals/update.py +5 -0
- firefighter/slack/views/modals/update_status.py +7 -5
- firefighter_fixtures/incidents/components.json +619 -491
- firefighter_fixtures/incidents/groups.json +64 -119
- firefighter_fixtures/incidents/impact_level.json +124 -47
- {firefighter_incident-0.0.1rc2.dist-info → firefighter_incident-0.0.3.dist-info}/METADATA +13 -8
- {firefighter_incident-0.0.1rc2.dist-info → firefighter_incident-0.0.3.dist-info}/RECORD +210 -195
- {firefighter_incident-0.0.1rc2.dist-info → firefighter_incident-0.0.3.dist-info}/WHEEL +1 -1
- firefighter_tests/conftest.py +8 -9
- firefighter_tests/test_api/test_api_urls.py +4 -4
- firefighter_tests/test_firefighter/test_urls.py +12 -12
- firefighter_tests/test_incidents/test_forms/test_form_select_impact.py +27 -27
- firefighter_tests/test_incidents/test_forms/test_form_utils.py +4 -5
- firefighter_tests/test_incidents/test_forms/test_update_key_events.py +3 -3
- firefighter_tests/test_incidents/test_incident_urls.py +10 -10
- firefighter_tests/test_incidents/test_models/test_incident_model.py +1 -1
- firefighter_tests/test_incidents/test_utils/test_date_utils.py +3 -2
- firefighter_tests/test_incidents/test_views/test_incident_detail_view.py +3 -3
- firefighter_tests/test_incidents/test_views/test_index_view.py +6 -6
- firefighter_tests/test_raid/test_raid_client_users.py +12 -12
- firefighter_tests/test_slack/conftest.py +6 -6
- firefighter_tests/test_slack/test_models/test_conversations.py +2 -2
- firefighter_tests/test_slack/test_models/test_incident_channel.py +55 -46
- firefighter_tests/test_slack/test_models/test_slack_user.py +9 -9
- firefighter_tests/test_slack/test_slack_utils.py +6 -6
- firefighter_tests/test_slack/views/modals/test_close.py +7 -7
- firefighter_tests/test_slack/views/modals/test_open.py +7 -7
- firefighter_tests/test_slack/views/modals/test_send_sos.py +2 -2
- firefighter_tests/test_slack/views/modals/test_status.py +2 -2
- firefighter_tests/test_slack/views/modals/test_update_status.py +4 -4
- manage.py +1 -0
- package-lock.json +6442 -0
- package.json +49 -0
- scripts/gen_credits.py +165 -0
- scripts/hatch_build.py +15 -0
- firefighter/raid/signals/update_qualifiers_rotation.py +0 -97
- firefighter/raid/tasks/daily_qualifier.py +0 -67
- firefighter/raid/tasks/weekly_qualifier.py +0 -63
- {firefighter_incident-0.0.1rc2.dist-info → firefighter_incident-0.0.3.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
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
|
-
|
|
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"])
|
firefighter/confluence/models.py
CHANGED
|
@@ -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) ->
|
|
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
|
-
) ->
|
|
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
|
|
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=
|
|
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
|
|
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
|
|
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
|
|
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 %}
|
firefighter/confluence/utils.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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 - {
|
|
34
|
+
err_msg = f'Task "{task_name}" does not exist. Available tasks are:\n - {sep.join(sorted(tasks))}'
|
|
35
35
|
raise CommandError(err_msg)
|
|
@@ -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"]
|
|
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)
|
firefighter/firefighter/sso.py
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
|
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
|
› <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
|
|
19
19
|
› {% 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 %}
|
firefighter/firefighter/urls.py
CHANGED
|
@@ -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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
firefighter/firefighter/utils.py
CHANGED
|
@@ -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
|
-
|
|
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
|
firefighter/firefighter/wsgi.py
CHANGED
|
@@ -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
|
|
21
|
+
from django.core.wsgi import get_wsgi_application
|
|
21
22
|
|
|
22
23
|
application = get_wsgi_application()
|
firefighter/incidents/admin.py
CHANGED
|
@@ -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,
|
|
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
|
-
) ->
|
|
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
|
|
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)
|
|
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,
|
|
584
|
+
self,
|
|
585
|
+
request: HttpRequest,
|
|
586
|
+
obj: User | None = None,
|
|
583
587
|
) -> _FieldsetSpec:
|
|
584
|
-
fieldsets = list(super().get_fieldsets(request, obj))
|
|
588
|
+
fieldsets = list(super().get_fieldsets(request, obj))
|
|
585
589
|
fieldsets.append(
|
|
586
590
|
(
|
|
587
591
|
_("User statistics"),
|