firefighter-incident 0.0.9__py3-none-any.whl → 0.0.11__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 +2 -2
- firefighter/components/avatar/avatar.py +1 -1
- firefighter/components/card/card.py +1 -1
- firefighter/components/export_button/export_button.py +1 -1
- firefighter/components/form/form.py +2 -2
- firefighter/components/form_field/form_field.py +1 -1
- firefighter/components/messages/messages.py +1 -1
- firefighter/components/modal/modal.py +1 -1
- firefighter/incidents/forms/select_impact.py +1 -1
- firefighter/incidents/migrations/0017_reorder_impact_types.py +28 -0
- firefighter/incidents/migrations/0018_update_impactlevel_names.py +48 -0
- firefighter/slack/views/modals/open.py +125 -52
- firefighter/slack/views/modals/opening/select_impact.py +38 -5
- {firefighter_incident-0.0.9.dist-info → firefighter_incident-0.0.11.dist-info}/METADATA +1 -1
- {firefighter_incident-0.0.9.dist-info → firefighter_incident-0.0.11.dist-info}/RECORD +19 -17
- firefighter_tests/test_slack/views/modals/test_open.py +25 -8
- {firefighter_incident-0.0.9.dist-info → firefighter_incident-0.0.11.dist-info}/WHEEL +0 -0
- {firefighter_incident-0.0.9.dist-info → firefighter_incident-0.0.11.dist-info}/entry_points.txt +0 -0
- {firefighter_incident-0.0.9.dist-info → firefighter_incident-0.0.11.dist-info}/licenses/LICENSE +0 -0
firefighter/_version.py
CHANGED
|
@@ -25,7 +25,7 @@ class Kwargs(TypedDict, total=False):
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
@component.register("avatar")
|
|
28
|
-
class Avatar(component.Component):
|
|
28
|
+
class Avatar(component.Component):
|
|
29
29
|
template_name = "avatar/avatar.html"
|
|
30
30
|
|
|
31
31
|
def get_context_data(self, user: User, **kwargs: Any) -> Data:
|
|
@@ -14,7 +14,7 @@ class Data(TypedDict, total=False):
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@component.register("card")
|
|
17
|
-
class Card(component.Component):
|
|
17
|
+
class Card(component.Component):
|
|
18
18
|
template_name = "card/card.html"
|
|
19
19
|
|
|
20
20
|
def get_context_data(self, *args: Any, **kwargs: Unpack[Data]) -> Data:
|
|
@@ -27,7 +27,7 @@ class Kwargs(TypedDict, total=False):
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
@component.register("export_button")
|
|
30
|
-
class ExportButton(component.Component):
|
|
30
|
+
class ExportButton(component.Component):
|
|
31
31
|
template_name = "export_button/export_button.html"
|
|
32
32
|
|
|
33
33
|
def get_context_data(
|
|
@@ -19,8 +19,8 @@ class Data(TypedDict):
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
@component.register("form")
|
|
22
|
-
class
|
|
22
|
+
class FormComponent(component.Component):
|
|
23
23
|
template_name = "form/form.html"
|
|
24
24
|
|
|
25
|
-
def get_context_data(self, form: forms.Form, **kwargs: Any) -> Data:
|
|
25
|
+
def get_context_data(self, form: forms.Form, **kwargs: Any) -> Data:
|
|
26
26
|
return Data(form=form)
|
|
@@ -22,7 +22,7 @@ class Kwargs(TypedDict, total=True):
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
@component.register("form_field")
|
|
25
|
-
class FormField(component.Component):
|
|
25
|
+
class FormField(component.Component):
|
|
26
26
|
template_name = "form_field/form_field.html"
|
|
27
27
|
|
|
28
28
|
def get_context_data(
|
|
@@ -14,7 +14,7 @@ class Data(TypedDict):
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@component.register("messages")
|
|
17
|
-
class
|
|
17
|
+
class MessagesComponent(component.Component):
|
|
18
18
|
template_name = "messages/messages.html"
|
|
19
19
|
|
|
20
20
|
def get_context_data(self, messages: BaseStorage, **kwargs: Any) -> Data:
|
|
@@ -28,7 +28,7 @@ class Slots(TypedDict):
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
@component.register("modal")
|
|
31
|
-
class Modal(component.Component):
|
|
31
|
+
class Modal(component.Component):
|
|
32
32
|
template_name = "modal/modal.html"
|
|
33
33
|
|
|
34
34
|
def get_context_data(self, *args: Any, **kwargs: Unpack[Kwargs]) -> Data:
|
|
@@ -39,7 +39,7 @@ class SelectImpactForm(forms.Form):
|
|
|
39
39
|
|
|
40
40
|
super().__init__(*args, **kwargs)
|
|
41
41
|
|
|
42
|
-
for impact_type in ImpactType.objects.all().order_by("
|
|
42
|
+
for impact_type in ImpactType.objects.all().order_by("order"):
|
|
43
43
|
field_name = f"set_impact_type_{impact_type.value}"
|
|
44
44
|
self.fields[field_name] = forms.ModelChoiceField(
|
|
45
45
|
label=impact_type.emoji + " " + impact_type.name,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Generated by ChatGPT on 2024-06-24
|
|
2
|
+
from django.db import migrations
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def reorder_impact_types(apps, schema_editor):
|
|
6
|
+
ImpactType = apps.get_model("incidents", "ImpactType")
|
|
7
|
+
# On suppose que les valeurs sont :
|
|
8
|
+
# business_impact, customers_impact, sellers_impact, employees_impact
|
|
9
|
+
order_map = {
|
|
10
|
+
"business_impact": 1,
|
|
11
|
+
"customers_impact": 2,
|
|
12
|
+
"sellers_impact": 3,
|
|
13
|
+
"employees_impact": 4,
|
|
14
|
+
}
|
|
15
|
+
for value, order in order_map.items():
|
|
16
|
+
impact_type = ImpactType.objects.filter(value=value).first()
|
|
17
|
+
if impact_type:
|
|
18
|
+
impact_type.order = order
|
|
19
|
+
impact_type.save(update_fields=["order"])
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Migration(migrations.Migration):
|
|
23
|
+
dependencies = [
|
|
24
|
+
("incidents", "0016_update_business_incidents_and_level"),
|
|
25
|
+
]
|
|
26
|
+
operations = [
|
|
27
|
+
migrations.RunPython(reorder_impact_types),
|
|
28
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Generated by ChatGPT on 2024-06-24
|
|
2
|
+
from django.db import migrations
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def update_impactlevel_names(apps, schema_editor):
|
|
6
|
+
ImpactType = apps.get_model("incidents", "ImpactType")
|
|
7
|
+
ImpactLevel = apps.get_model("incidents", "ImpactLevel")
|
|
8
|
+
|
|
9
|
+
# Map: (impact_type.value, impact_level.value) -> new name
|
|
10
|
+
updates = {
|
|
11
|
+
("business_impact", "HT"): "Critical Impact (>5% BV loss)",
|
|
12
|
+
("business_impact", "HI"): "Major Impact (<5% BV loss)",
|
|
13
|
+
("business_impact", "MD"): "Uncertain Business Impact",
|
|
14
|
+
("business_impact", "LO"): "Low Impact not measurable",
|
|
15
|
+
("business_impact", "LT"): "Very Low Impact not measurable",
|
|
16
|
+
("customers_impact", "HT"): "Critical issue for many customers",
|
|
17
|
+
("customers_impact", "HI"): "Major issue for many customers",
|
|
18
|
+
("customers_impact", "MD"): "Major issue for few customers",
|
|
19
|
+
("customers_impact", "LO"): "Minor issue for customers",
|
|
20
|
+
("customers_impact", "LT"): "Cosmetic minor issue for customers",
|
|
21
|
+
("sellers_impact", "HT"): "Critical issue for many sellers",
|
|
22
|
+
("sellers_impact", "HI"): "Major issue for many sellers",
|
|
23
|
+
("sellers_impact", "MD"): "Major issue for few sellers",
|
|
24
|
+
("sellers_impact", "LO"): "Minor issue for sellers",
|
|
25
|
+
("sellers_impact", "LT"): "Cosmetic minor issue for sellers",
|
|
26
|
+
("employees_impact", "MD"): "Departments fully blocked",
|
|
27
|
+
("employees_impact", "LO"): "Some employees affected",
|
|
28
|
+
("employees_impact", "LT"): "Degraded service for employees",
|
|
29
|
+
("employees_impact", "NO"): "No impact for employees",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (impact_type_value, level_value), new_name in updates.items():
|
|
33
|
+
impact_type = ImpactType.objects.filter(value=impact_type_value).first()
|
|
34
|
+
if not impact_type:
|
|
35
|
+
continue
|
|
36
|
+
impact_level = ImpactLevel.objects.filter(impact_type=impact_type, value=level_value).first()
|
|
37
|
+
if impact_level:
|
|
38
|
+
impact_level.name = new_name
|
|
39
|
+
impact_level.save(update_fields=["name"])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Migration(migrations.Migration):
|
|
43
|
+
dependencies = [
|
|
44
|
+
("incidents", "0017_reorder_impact_types"),
|
|
45
|
+
]
|
|
46
|
+
operations = [
|
|
47
|
+
migrations.RunPython(update_impactlevel_names),
|
|
48
|
+
]
|
|
@@ -12,7 +12,6 @@ from django.utils.translation import ngettext
|
|
|
12
12
|
from slack_sdk.models.blocks.basic_components import MarkdownTextObject, Option
|
|
13
13
|
from slack_sdk.models.blocks.block_elements import ButtonElement, StaticSelectElement
|
|
14
14
|
from slack_sdk.models.blocks.blocks import (
|
|
15
|
-
ActionsBlock,
|
|
16
15
|
Block,
|
|
17
16
|
ContextBlock,
|
|
18
17
|
DividerBlock,
|
|
@@ -23,6 +22,7 @@ from slack_sdk.models.views import View
|
|
|
23
22
|
from firefighter.firefighter.utils import is_during_office_hours
|
|
24
23
|
from firefighter.incidents.enums import IncidentStatus
|
|
25
24
|
from firefighter.incidents.forms.select_impact import SelectImpactForm
|
|
25
|
+
from firefighter.incidents.models.impact import ImpactType
|
|
26
26
|
from firefighter.incidents.models.incident import Incident
|
|
27
27
|
from firefighter.incidents.models.priority import Priority
|
|
28
28
|
from firefighter.slack.slack_app import SlackApp
|
|
@@ -73,7 +73,9 @@ class OpenModal(SlackModal):
|
|
|
73
73
|
# 1. Check if impact form is good
|
|
74
74
|
is_impact_form_valid: bool = self._check_impact_form(open_incident_context)
|
|
75
75
|
|
|
76
|
-
# 2.
|
|
76
|
+
# 2. Auto-determine response type based on priority
|
|
77
|
+
self._auto_set_response_type(open_incident_context)
|
|
78
|
+
|
|
77
79
|
incident_type_value: str | None = open_incident_context.get(
|
|
78
80
|
"incident_type", None
|
|
79
81
|
)
|
|
@@ -180,11 +182,18 @@ class OpenModal(SlackModal):
|
|
|
180
182
|
SelectImpactModal,
|
|
181
183
|
)
|
|
182
184
|
|
|
185
|
+
# Check if we have actual impacts (not all "NO") by checking if response_type is set
|
|
186
|
+
has_real_impacts = open_incident_context.get("response_type") is not None
|
|
187
|
+
|
|
188
|
+
# Show ✅ only if form is valid AND has real impacts, otherwise 📝
|
|
189
|
+
emoji = "✅" if impact_form_done and has_real_impacts else "📝"
|
|
190
|
+
button_text = "Edit impacts" if impact_form_done and has_real_impacts else "Set impacts"
|
|
191
|
+
|
|
183
192
|
return [
|
|
184
193
|
SectionBlock(
|
|
185
|
-
text=f"{
|
|
194
|
+
text=f"{emoji} First, define the incident impacts and priority.",
|
|
186
195
|
accessory=ButtonElement(
|
|
187
|
-
text=
|
|
196
|
+
text=button_text,
|
|
188
197
|
action_id=SelectImpactModal.push_action,
|
|
189
198
|
value=json.dumps(open_incident_context, cls=SlackFormJSONEncoder),
|
|
190
199
|
),
|
|
@@ -370,58 +379,79 @@ class OpenModal(SlackModal):
|
|
|
370
379
|
|
|
371
380
|
return is_valid, details_form_class, details_form
|
|
372
381
|
|
|
382
|
+
@staticmethod
|
|
383
|
+
def _auto_set_response_type(open_incident_context: OpeningData) -> None:
|
|
384
|
+
"""Auto-determine response type based on priority from impact form."""
|
|
385
|
+
impact_form_data = open_incident_context.get("impact_form_data")
|
|
386
|
+
if not impact_form_data:
|
|
387
|
+
# Clear response_type and priority if no impact data
|
|
388
|
+
open_incident_context.pop("response_type", None)
|
|
389
|
+
open_incident_context.pop("priority", None)
|
|
390
|
+
return
|
|
391
|
+
|
|
392
|
+
impact_form = SelectImpactForm(impact_form_data)
|
|
393
|
+
if not impact_form.is_valid():
|
|
394
|
+
# Clear response_type and priority if form is invalid
|
|
395
|
+
open_incident_context.pop("response_type", None)
|
|
396
|
+
open_incident_context.pop("priority", None)
|
|
397
|
+
return
|
|
398
|
+
|
|
399
|
+
priority_value = impact_form.suggest_priority_from_impact()
|
|
400
|
+
|
|
401
|
+
# If no impacts are selected (all set to "NO"), don't set priority/response_type
|
|
402
|
+
# Priority value 6 corresponds to LevelChoices.NONE.priority
|
|
403
|
+
if priority_value == 6:
|
|
404
|
+
open_incident_context.pop("response_type", None)
|
|
405
|
+
open_incident_context.pop("priority", None)
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
priority = Priority.objects.get(value=priority_value)
|
|
409
|
+
|
|
410
|
+
# Set priority in context
|
|
411
|
+
open_incident_context["priority"] = priority
|
|
412
|
+
|
|
413
|
+
# Set response type based on priority recommendation
|
|
414
|
+
if priority.recommended_response_type:
|
|
415
|
+
open_incident_context["response_type"] = cast("ResponseType | None", priority.recommended_response_type)
|
|
416
|
+
else:
|
|
417
|
+
# Default fallback: P1/P2/P3 = critical, P4/P5 = normal
|
|
418
|
+
response_type = cast("ResponseType", "critical" if priority_value < 4 else "normal")
|
|
419
|
+
open_incident_context["response_type"] = response_type
|
|
420
|
+
|
|
373
421
|
@staticmethod
|
|
374
422
|
def _build_response_type_blocks(open_incident_context: OpeningData) -> list[Block]:
|
|
375
423
|
selected_response_type = open_incident_context.get("response_type")
|
|
376
424
|
if selected_response_type not in {"critical", "normal"}:
|
|
377
425
|
return []
|
|
378
426
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
)
|
|
382
|
-
elements: list[ButtonElement] = []
|
|
383
|
-
|
|
384
|
-
for response_type in response_types:
|
|
385
|
-
if response_type != selected_response_type:
|
|
386
|
-
continue
|
|
387
|
-
|
|
388
|
-
is_selected = (
|
|
389
|
-
open_incident_context.get("response_type") == response_type
|
|
390
|
-
or len(INCIDENT_TYPES) == 1
|
|
391
|
-
)
|
|
392
|
-
style: str | None = "primary" if is_selected else None
|
|
393
|
-
text = (
|
|
394
|
-
":slack: Slack :jira_new: Jira ticket"
|
|
395
|
-
if response_type == "critical"
|
|
396
|
-
else ":jira_new: Jira ticket"
|
|
397
|
-
)
|
|
398
|
-
button = ButtonElement(
|
|
399
|
-
text=text,
|
|
400
|
-
action_id=f"incident_open_set_res_type_{response_type}",
|
|
401
|
-
value=json.dumps(open_incident_context, cls=SlackFormJSONEncoder),
|
|
402
|
-
style=style,
|
|
403
|
-
)
|
|
404
|
-
elements.append(button)
|
|
405
|
-
|
|
406
|
-
blocks: list[Block] = [ActionsBlock(elements=elements)]
|
|
427
|
+
blocks: list[Block] = []
|
|
428
|
+
# No buttons needed - response type is auto-determined
|
|
407
429
|
if impact_form_data := open_incident_context.get("impact_form_data"):
|
|
408
430
|
impact_form = SelectImpactForm(impact_form_data)
|
|
409
431
|
if impact_form.is_valid():
|
|
410
432
|
priority: Priority = Priority.objects.get(
|
|
411
433
|
value=impact_form.suggest_priority_from_impact()
|
|
412
434
|
)
|
|
435
|
+
process = ":slack: Slack :jira_new: Jira ticket" if open_incident_context.get("response_type") == "critical" else ":jira_new: Jira ticket"
|
|
436
|
+
|
|
437
|
+
impact_descriptions = OpenModal._get_impact_descriptions(open_incident_context)
|
|
438
|
+
|
|
413
439
|
blocks.append(
|
|
414
440
|
ContextBlock(
|
|
415
441
|
elements=[
|
|
416
442
|
MarkdownTextObject(
|
|
417
|
-
text=f"> {priority.emoji} Selected priority: {priority}"
|
|
443
|
+
text=f"> {priority.emoji} Selected priority: *{priority} - {priority.description}*\n"
|
|
444
|
+
f"> ⏱️ SLA: {priority.sla}\n"
|
|
445
|
+
f"> :gear: Process: {process}\n"
|
|
446
|
+
f"> :pushpin: Selected impacts:\n"
|
|
447
|
+
f"{impact_descriptions}"
|
|
418
448
|
+ (
|
|
419
449
|
(
|
|
420
|
-
"
|
|
450
|
+
"> :warning: Critical incidents are for *emergency* only"
|
|
421
451
|
+ (
|
|
422
|
-
f"<{SLACK_SEVERITY_HELP_GUIDE_URL}|
|
|
452
|
+
f" <{SLACK_SEVERITY_HELP_GUIDE_URL}|more info>"
|
|
423
453
|
if SLACK_SEVERITY_HELP_GUIDE_URL
|
|
424
|
-
else "
|
|
454
|
+
else "."
|
|
425
455
|
)
|
|
426
456
|
)
|
|
427
457
|
if selected_response_type == "critical"
|
|
@@ -448,6 +478,59 @@ class OpenModal(SlackModal):
|
|
|
448
478
|
|
|
449
479
|
return blocks
|
|
450
480
|
|
|
481
|
+
@staticmethod
|
|
482
|
+
def _get_impact_descriptions(open_incident_context: OpeningData) -> str:
|
|
483
|
+
impact_form_data = open_incident_context.get("impact_form_data", {})
|
|
484
|
+
if not impact_form_data:
|
|
485
|
+
return ""
|
|
486
|
+
|
|
487
|
+
impact_descriptions = ""
|
|
488
|
+
for field_name, original_value in impact_form_data.items():
|
|
489
|
+
value = original_value
|
|
490
|
+
# Handle case where value might be an ID instead of an object
|
|
491
|
+
if isinstance(value, int | str) and not hasattr(value, "name"):
|
|
492
|
+
# Try to get the object from the database
|
|
493
|
+
form = SelectImpactForm()
|
|
494
|
+
if field_name in form.fields:
|
|
495
|
+
field = form.fields[field_name]
|
|
496
|
+
if hasattr(field, "queryset") and field.queryset is not None:
|
|
497
|
+
try:
|
|
498
|
+
value = field.queryset.get(pk=value)
|
|
499
|
+
except field.queryset.model.DoesNotExist:
|
|
500
|
+
logger.warning(f"Could not find impact object with pk={value} for field {field_name}")
|
|
501
|
+
continue
|
|
502
|
+
|
|
503
|
+
description = OpenModal._format_single_impact_description(value)
|
|
504
|
+
if description:
|
|
505
|
+
impact_descriptions += description
|
|
506
|
+
return impact_descriptions
|
|
507
|
+
|
|
508
|
+
@staticmethod
|
|
509
|
+
def _format_single_impact_description(value: Any) -> str:
|
|
510
|
+
"""Format a single impact value into description text."""
|
|
511
|
+
# Handle object with name and description attributes (impact levels)
|
|
512
|
+
if hasattr(value, "name") and hasattr(value, "description"):
|
|
513
|
+
if value.name == "NO" or not value.description:
|
|
514
|
+
return ""
|
|
515
|
+
|
|
516
|
+
description = ""
|
|
517
|
+
# Add impact type header if available
|
|
518
|
+
if hasattr(value, "impact_type_id") and value.impact_type_id:
|
|
519
|
+
try:
|
|
520
|
+
impact_type = ImpactType.objects.get(pk=value.impact_type_id)
|
|
521
|
+
# Use value.name instead of value to avoid showing IDs
|
|
522
|
+
description += f"> \u00A0\u00A0 :exclamation: {impact_type} - {value.name}\n"
|
|
523
|
+
except ImpactType.DoesNotExist:
|
|
524
|
+
description += f"> \u00A0\u00A0 :exclamation: {value.name}\n"
|
|
525
|
+
|
|
526
|
+
# Add description lines
|
|
527
|
+
for line in str(value.description).splitlines():
|
|
528
|
+
description += f"> \u00A0\u00A0\u00A0\u00A0\u00A0\u00A0 • {line}\n"
|
|
529
|
+
return description
|
|
530
|
+
|
|
531
|
+
# Skip string values - incident_type is handled separately, not in impact descriptions
|
|
532
|
+
return ""
|
|
533
|
+
|
|
451
534
|
@staticmethod
|
|
452
535
|
def get_details_modal_form_class(
|
|
453
536
|
open_incident_context: OpeningData,
|
|
@@ -508,21 +591,7 @@ class OpenModal(SlackModal):
|
|
|
508
591
|
logger.exception("Error triggering incident workflow")
|
|
509
592
|
# XXX warn the user via DM!
|
|
510
593
|
|
|
511
|
-
|
|
512
|
-
@app.action("incident_open_set_res_type_critical")
|
|
513
|
-
@staticmethod
|
|
514
|
-
def handle_set_incident_response_type_action(
|
|
515
|
-
ack: Ack, body: dict[str, Any]
|
|
516
|
-
) -> None:
|
|
517
|
-
action_name: str = body.get("actions", [{}])[0].get("action_id", "")
|
|
518
|
-
action_name = action_name.replace("incident_open_set_res_type_", "")
|
|
519
|
-
opening_data = cast(
|
|
520
|
-
"OpeningData", json.loads(body.get("actions", [{}])[0].get("value", {})) or {}
|
|
521
|
-
)
|
|
522
|
-
|
|
523
|
-
OpenModal._update_incident_modal(
|
|
524
|
-
action_name, "response_type", ack, body, opening_data
|
|
525
|
-
)
|
|
594
|
+
# Response type buttons removed - now auto-determined based on priority
|
|
526
595
|
|
|
527
596
|
@app.action("set_type")
|
|
528
597
|
@staticmethod
|
|
@@ -544,7 +613,11 @@ class OpenModal(SlackModal):
|
|
|
544
613
|
body: dict[str, Any],
|
|
545
614
|
opening_data: OpeningData,
|
|
546
615
|
) -> None:
|
|
547
|
-
|
|
616
|
+
# Ensure we preserve all existing data, especially impact_form_data
|
|
617
|
+
data: OpeningData = OpeningData()
|
|
618
|
+
data.update(opening_data)
|
|
619
|
+
data[metadata_key] = action_value
|
|
620
|
+
|
|
548
621
|
user = get_user_from_context(body)
|
|
549
622
|
view = cls().build_modal_fn(open_incident_context=data, user=user)
|
|
550
623
|
|
|
@@ -205,7 +205,16 @@ class SelectImpactModal(
|
|
|
205
205
|
def _calculate_proposed_incident_type(
|
|
206
206
|
suggested_priority_value: int,
|
|
207
207
|
) -> ResponseType:
|
|
208
|
-
|
|
208
|
+
try:
|
|
209
|
+
priority = Priority.objects.get(value=suggested_priority_value)
|
|
210
|
+
# Use priority recommendation if available
|
|
211
|
+
if priority.recommended_response_type:
|
|
212
|
+
return cast("ResponseType", priority.recommended_response_type)
|
|
213
|
+
except Priority.DoesNotExist:
|
|
214
|
+
logger.warning(f"Priority with value {suggested_priority_value} does not exist")
|
|
215
|
+
|
|
216
|
+
# Fallback logic: P1/P2/P3 = critical, P4/P5 = normal
|
|
217
|
+
return cast("ResponseType", "critical" if suggested_priority_value < 4 else "normal")
|
|
209
218
|
|
|
210
219
|
@staticmethod
|
|
211
220
|
def _update_private_metadata(
|
|
@@ -230,12 +239,36 @@ class SelectImpactModal(
|
|
|
230
239
|
)
|
|
231
240
|
except queryset.model.DoesNotExist:
|
|
232
241
|
form.form.data[field_name] = None # type: ignore
|
|
242
|
+
suggested_priority_value = form.form.suggest_priority_from_impact()
|
|
243
|
+
|
|
244
|
+
# If no impacts are selected (all set to "NO"), don't set priority/response_type
|
|
245
|
+
if suggested_priority_value == 6: # LevelChoices.NONE.priority
|
|
246
|
+
return OpeningData(
|
|
247
|
+
priority=None,
|
|
248
|
+
response_type=None,
|
|
249
|
+
impact_form_data=cast("dict[str, Any]", form.form.data),
|
|
250
|
+
details_form_data=private_metadata_raw.get("details_form_data", {}),
|
|
251
|
+
incident_type=private_metadata_raw.get("incident_type"),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
priority = Priority.objects.get(value=suggested_priority_value)
|
|
256
|
+
except Priority.DoesNotExist as err:
|
|
257
|
+
logger.exception(
|
|
258
|
+
f"Priority with value {suggested_priority_value} does not exist"
|
|
259
|
+
)
|
|
260
|
+
# Fallback to default priority (assuming P3 exists)
|
|
261
|
+
fallback_priority = Priority.objects.filter(value__gte=3).first()
|
|
262
|
+
if not fallback_priority:
|
|
263
|
+
# If no priority exists, create a minimal fallback
|
|
264
|
+
logger.exception("No priority found in database")
|
|
265
|
+
raise ValueError("No priority configuration found in database") from err
|
|
266
|
+
priority = fallback_priority
|
|
267
|
+
|
|
233
268
|
return OpeningData(
|
|
234
|
-
priority=
|
|
235
|
-
value=form.form.suggest_priority_from_impact()
|
|
236
|
-
),
|
|
269
|
+
priority=priority,
|
|
237
270
|
response_type=SelectImpactModal._calculate_proposed_incident_type(
|
|
238
|
-
|
|
271
|
+
suggested_priority_value
|
|
239
272
|
),
|
|
240
273
|
impact_form_data=cast("dict[str, Any]", form.form.data),
|
|
241
274
|
details_form_data=private_metadata_raw.get("details_form_data", {}),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: firefighter-incident
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.11
|
|
4
4
|
Summary: Incident Management tool made for Slack using Django
|
|
5
5
|
Project-URL: Repository, https://github.com/ManoManoTech/firefighter-incident
|
|
6
6
|
Project-URL: Documentation, https://manomanotech.github.io/firefighter-incident/latest/
|
|
@@ -6,7 +6,7 @@ gunicorn.conf.py,sha256=vHsTGjaKOr8FDMp6fTKYTX4AtokmPgYvvt5Mr0Q6APc,273
|
|
|
6
6
|
main.py,sha256=CsbprHoOYhjCLpTJmq9Z_aRYFoFgWxoz2pDLuwm8Eqg,1558
|
|
7
7
|
manage.py,sha256=5ivHGD13C6nJ8QvltKsJ9T9akA5he8da70HLWaEP3k8,689
|
|
8
8
|
firefighter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
firefighter/_version.py,sha256=
|
|
9
|
+
firefighter/_version.py,sha256=rk0lhpp6Em5toAI4J7GwApfOdY7w_QTcFpJpUR4GdVY,513
|
|
10
10
|
firefighter/api/__init__.py,sha256=JQW0Bv6xwGqy7ioxx3h6UGMzkkJ4DntDpbvV1Ncgi8k,136
|
|
11
11
|
firefighter/api/admin.py,sha256=x9Ysy-GiYjb0rynmFdS9g56e6n24fkN0ouGy5QD9Yrc,4629
|
|
12
12
|
firefighter/api/apps.py,sha256=P5uU1_gMrDfzurdMbfqw1Bnb2uNKKcMq17WBPg2sLhc,204
|
|
@@ -32,25 +32,25 @@ firefighter/api/views/severities.py,sha256=mdkR4GjZibydC1Dx-Sglm-f35GZxWbjmqStAx
|
|
|
32
32
|
firefighter/components/__init__.py,sha256=Vd_Uk5Uq7Mqp6NOFp5QiniWZAyzmYLqNSFEEw1x7COk,167
|
|
33
33
|
firefighter/components/avatar/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
34
|
firefighter/components/avatar/avatar.html,sha256=oleFPTy1qs9X6hZx3iWppBteHummFvSxoNzPOOl5aeA,773
|
|
35
|
-
firefighter/components/avatar/avatar.py,sha256=
|
|
35
|
+
firefighter/components/avatar/avatar.py,sha256=AIqffX8I_sJ7oEpuyIswJkB6KuW51FuEa4tBn9eSths,823
|
|
36
36
|
firefighter/components/card/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
37
|
firefighter/components/card/card.html,sha256=ziahql8X7WiQdprMktzy7sx0gVXNIxqtAsh_Rc5Iy0g,725
|
|
38
|
-
firefighter/components/card/card.py,sha256=
|
|
38
|
+
firefighter/components/card/card.py,sha256=yB4veoRQ3zfdJPCsAGIYtmLCDz3G2kmcYFg12jzkNks,487
|
|
39
39
|
firefighter/components/export_button/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
40
40
|
firefighter/components/export_button/export_button.html,sha256=XTA9FbDxho8aIqnfTcsDUm7swGWfn2JSw5WndDaThYY,1989
|
|
41
|
-
firefighter/components/export_button/export_button.py,sha256=
|
|
41
|
+
firefighter/components/export_button/export_button.py,sha256=AqkkaZFGVzuLKpIxcPZ-vcXG9VXf9jqpfyxYePiGe4E,1703
|
|
42
42
|
firefighter/components/form/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
43
43
|
firefighter/components/form/form.html,sha256=LtW07AxFsR-MI-iGVq0CsUVsUN3af5rS2R4SbAlfI5s,312
|
|
44
|
-
firefighter/components/form/form.py,sha256=
|
|
44
|
+
firefighter/components/form/form.py,sha256=92qb1HTO3YvYlQMsw6uN8QUQZpbRbyGaCnPGuGX7VDs,596
|
|
45
45
|
firefighter/components/form_field/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
46
|
firefighter/components/form_field/form_field.html,sha256=k5yyxvETqJSt5jHb8Z2v5o9oJRDLwWBKhISy0j0184Y,450
|
|
47
|
-
firefighter/components/form_field/form_field.py,sha256=
|
|
47
|
+
firefighter/components/form_field/form_field.py,sha256=QZrmEh-3wN8TUdx6db9_8rSpKuTfbyQ-1K0N7st4CYg,734
|
|
48
48
|
firefighter/components/messages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
49
49
|
firefighter/components/messages/messages.html,sha256=P0V6Wsz7lTZL0xNPu_5w-J6Dc_aPkP0wqbHjMJ1D4qs,3157
|
|
50
|
-
firefighter/components/messages/messages.py,sha256=
|
|
50
|
+
firefighter/components/messages/messages.py,sha256=vQ1aaIkRo9T9xtIfYneelrUbBw0cf9BOoaphEznmWS4,519
|
|
51
51
|
firefighter/components/modal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
52
52
|
firefighter/components/modal/modal.html,sha256=PrOtS8eJHvzTHqILlXmjE4KsGVdbubSCexHiPa_I3Yk,2391
|
|
53
|
-
firefighter/components/modal/modal.py,sha256=
|
|
53
|
+
firefighter/components/modal/modal.py,sha256=rpleZPmOWNCSQAEXTsQRfETwbyodjT3o7ni-pIqMWiI,869
|
|
54
54
|
firefighter/confluence/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
55
55
|
firefighter/confluence/admin.py,sha256=aDXghuuLc7G_TLt-655M31smx-H6vkIgLtEmmNCA3lg,1490
|
|
56
56
|
firefighter/confluence/apps.py,sha256=vKswBwQL7L9e2JQwvRb7xy3myyE_GRldYX78jSY3XCM,406
|
|
@@ -140,7 +140,7 @@ firefighter/incidents/forms/__init__.py,sha256=OU0r5eZc2A0UJNsL83n8AI5EvwUvg4Yx0
|
|
|
140
140
|
firefighter/incidents/forms/close_incident.py,sha256=syT5Lpr_WXNFT3KGCe1oy-FzOqMt98S7YEzovdnp7To,940
|
|
141
141
|
firefighter/incidents/forms/create_incident.py,sha256=Wpp0qqUJQs5-5BXrS-P5-dGvM5zgr9XqaEEl6tpNZi4,2739
|
|
142
142
|
firefighter/incidents/forms/edit.py,sha256=2rQkiKak-vac-K3cIsqlGv4R5nhI7JLxw3DhFMXbWms,956
|
|
143
|
-
firefighter/incidents/forms/select_impact.py,sha256=
|
|
143
|
+
firefighter/incidents/forms/select_impact.py,sha256=jLbzVj4UeUGwOYYa5P92PXkEu1J_6H43UATZYzDgSLY,4630
|
|
144
144
|
firefighter/incidents/forms/update_key_events.py,sha256=1Xmnxe5OgZqLFS2HmMzQm3VGFPQipsdrLgKSwdh-fKc,4441
|
|
145
145
|
firefighter/incidents/forms/update_roles.py,sha256=Q26UPfwAj-8N23RNZLQkvmHGnS1_j_X5KQWjJmPjMKY,3635
|
|
146
146
|
firefighter/incidents/forms/update_status.py,sha256=QCRKfDhSYZhVsJ6oofQxOXGMWMDRQEDnH29y8YnFn_Y,1034
|
|
@@ -161,6 +161,8 @@ firefighter/incidents/migrations/0013_add_missing_component.py,sha256=qVLQEl-riF
|
|
|
161
161
|
firefighter/incidents/migrations/0014_update_components_slack_groups.py,sha256=5tiQnrtOpYVUztFdvZ6xTUReuJfpDX0cOvqIvDZpBv0,8700
|
|
162
162
|
firefighter/incidents/migrations/0015_update_impact_level.py,sha256=OQVTVrWvQ1orxqdrqwrhBMjxKY5qzqN2ZRobHc_tGDc,5451
|
|
163
163
|
firefighter/incidents/migrations/0016_update_business_incidents_and_level.py,sha256=O5AL9twmjstSw44ndJ2-Og6dpKreReXVw_brpQb-t0w,3600
|
|
164
|
+
firefighter/incidents/migrations/0017_reorder_impact_types.py,sha256=7NN2KjcDRHNInXyEh8YKZli2LWT_i0UHXBbI21dXB6w,877
|
|
165
|
+
firefighter/incidents/migrations/0018_update_impactlevel_names.py,sha256=E37png_LiXABzuXk-vmxTgikw8YmULODkiytE2mkxT0,2227
|
|
164
166
|
firefighter/incidents/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
165
167
|
firefighter/incidents/models/__init__.py,sha256=dCNP-zRYNNDOZB3JDDWp7vCl084Jh6RgDT_iP57RkOY,862
|
|
166
168
|
firefighter/incidents/models/component.py,sha256=7GyXKNFk1MZns6RUGLpkNw5u6He7H9N1LexzXbG4sBM,7445
|
|
@@ -383,7 +385,7 @@ firefighter/slack/views/modals/close.py,sha256=ur1SSRWk9NYFfL24gjOqoIiXKquDy6qeE
|
|
|
383
385
|
firefighter/slack/views/modals/downgrade_workflow.py,sha256=S0y0_GYH4q7ewZUr_eA9Ly2c1FQueZzNCTiuIiWYUoY,3109
|
|
384
386
|
firefighter/slack/views/modals/edit.py,sha256=60xav4XG4KGS9KknqsQNCQjl3qQzk7OtmHiEYTQ9pUk,3861
|
|
385
387
|
firefighter/slack/views/modals/key_event_message.py,sha256=ga3-ITZyzJExwzctX-GfgnDqyQaxTfcqpqnOwY2E38M,5620
|
|
386
|
-
firefighter/slack/views/modals/open.py,sha256=
|
|
388
|
+
firefighter/slack/views/modals/open.py,sha256=LX9aBZ4bUosoffJlIepzYjpbf7LsvzppYAjqep8tVtM,25495
|
|
387
389
|
firefighter/slack/views/modals/postmortem.py,sha256=AeEtmiam_XgCRxDmltKluNT2VN1gcuCB2VbYeeATVcA,2525
|
|
388
390
|
firefighter/slack/views/modals/select.py,sha256=Y-Ji_ALnzhYkXDBAyi497UL1Xn2vCGqXCtj8eog75Jk,3312
|
|
389
391
|
firefighter/slack/views/modals/send_sos.py,sha256=bP6HgYyDwPrIcTq7n_sQz6UQsxhYbvBDS4HjM0uRccA,4838
|
|
@@ -400,7 +402,7 @@ firefighter/slack/views/modals/base_modal/mixins.py,sha256=c7WYs0aXKXVktEMNSZ8IU
|
|
|
400
402
|
firefighter/slack/views/modals/base_modal/modal_utils.py,sha256=1uHTlLxxeXUQttH3bHaehJwCuI6a-h04s-GzdnVA4sI,2459
|
|
401
403
|
firefighter/slack/views/modals/opening/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
402
404
|
firefighter/slack/views/modals/opening/check_current_incidents.py,sha256=28GN0SXP7rVPa55arX1aI98k45w9568GCRDA73eCHEM,2535
|
|
403
|
-
firefighter/slack/views/modals/opening/select_impact.py,sha256=
|
|
405
|
+
firefighter/slack/views/modals/opening/select_impact.py,sha256=McVKE5z8vjcg0Z1kbqTsXBW9FvTqX02W6HiIPQ8cicI,11424
|
|
404
406
|
firefighter/slack/views/modals/opening/set_details.py,sha256=i6zQM2FYz3Z6s5AZH7lXgB2e8yjS0rDwgfMBZaiOqIw,5791
|
|
405
407
|
firefighter/slack/views/modals/opening/types.py,sha256=ETpp0DAz5OMI5h7iv62Of7yJCbI-Q4-3kKSS6msPQeY,563
|
|
406
408
|
firefighter/slack/views/modals/opening/details/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -442,12 +444,12 @@ firefighter_tests/test_slack/test_models/test_conversations.py,sha256=t3ttmgwiu7
|
|
|
442
444
|
firefighter_tests/test_slack/test_models/test_incident_channel.py,sha256=qWoGe9iadmK6-R8usWvjH87AHRkvhG_dHQeC3kHeJrs,17487
|
|
443
445
|
firefighter_tests/test_slack/test_models/test_slack_user.py,sha256=uzur-Rf03I5dpUTO4ZI6O1arBUrAorg1Zvgshf8M-J4,7000
|
|
444
446
|
firefighter_tests/test_slack/views/modals/test_close.py,sha256=kcwGwonjIiniGb5f78ZwlKjuvYB-xat-SrbouV9VCEc,42894
|
|
445
|
-
firefighter_tests/test_slack/views/modals/test_open.py,sha256=
|
|
447
|
+
firefighter_tests/test_slack/views/modals/test_open.py,sha256=z3lvAPOXCUSt7i_9jWYcQWGIRwRg7Z1DT6AfMOK22_s,4900
|
|
446
448
|
firefighter_tests/test_slack/views/modals/test_send_sos.py,sha256=_rE6jD-gOzcGyhlY0R9GzlGtPx65oOOguJYdENgxtLc,1289
|
|
447
449
|
firefighter_tests/test_slack/views/modals/test_status.py,sha256=oQzPfwdg2tkbo9nfkO1GfS3WydxqSC6vy1AZjZDKT30,2226
|
|
448
450
|
firefighter_tests/test_slack/views/modals/test_update_status.py,sha256=Y8Oa_fraj1vtaGig9Y28_6tOWvMrRPS-wyg3rY-DHBk,39380
|
|
449
|
-
firefighter_incident-0.0.
|
|
450
|
-
firefighter_incident-0.0.
|
|
451
|
-
firefighter_incident-0.0.
|
|
452
|
-
firefighter_incident-0.0.
|
|
453
|
-
firefighter_incident-0.0.
|
|
451
|
+
firefighter_incident-0.0.11.dist-info/METADATA,sha256=9Xvtj0AnQmzza1kfVGGGEMcJJEokzGn9vUs6FzslagA,5488
|
|
452
|
+
firefighter_incident-0.0.11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
453
|
+
firefighter_incident-0.0.11.dist-info/entry_points.txt,sha256=c13meJbv7YNmYz7MipMOQwzQ5IeFOPXUBYAJ44XMQsM,61
|
|
454
|
+
firefighter_incident-0.0.11.dist-info/licenses/LICENSE,sha256=krRiGp-a9-1nH1bWpBEdxyTKLhjLmn6DMVVoIb0zF90,1087
|
|
455
|
+
firefighter_incident-0.0.11.dist-info/RECORD,,
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from typing import Any
|
|
4
|
-
from unittest.mock import MagicMock
|
|
4
|
+
from unittest.mock import MagicMock, Mock, patch
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
|
-
from slack_sdk.models.blocks.block_elements import ButtonElement
|
|
8
7
|
from slack_sdk.models.blocks.blocks import (
|
|
9
|
-
|
|
8
|
+
ContextBlock,
|
|
10
9
|
)
|
|
11
10
|
|
|
12
11
|
from firefighter.incidents.forms.create_incident import CreateIncidentFormBase
|
|
@@ -102,14 +101,32 @@ def test_validate_details_form_invalid() -> None:
|
|
|
102
101
|
|
|
103
102
|
|
|
104
103
|
def test_build_response_type_blocks_bis(open_incident_context: OpeningData) -> None:
|
|
104
|
+
# With no impact_form_data, should return empty list
|
|
105
105
|
open_incident_context["response_type"] = "critical"
|
|
106
106
|
blocks = OpenModal._build_response_type_blocks(open_incident_context)
|
|
107
|
+
assert len(blocks) == 0
|
|
107
108
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
# With valid impact_form_data, should return context blocks
|
|
110
|
+
mock_impact_form = Mock()
|
|
111
|
+
mock_impact_form.is_valid.return_value = True
|
|
112
|
+
mock_impact_form.suggest_priority_from_impact.return_value = 1
|
|
113
|
+
|
|
114
|
+
# Mock Priority object
|
|
115
|
+
mock_priority = Mock()
|
|
116
|
+
mock_priority.emoji = "🔴"
|
|
117
|
+
mock_priority.description = "Critical"
|
|
118
|
+
mock_priority.sla = "15 min"
|
|
119
|
+
mock_priority.recommended_response_type = None
|
|
120
|
+
|
|
121
|
+
open_incident_context["impact_form_data"] = {"test_field": "test_value"}
|
|
122
|
+
|
|
123
|
+
with patch("firefighter.slack.views.modals.open.SelectImpactForm", return_value=mock_impact_form), \
|
|
124
|
+
patch("firefighter.slack.views.modals.open.Priority.objects.get", return_value=mock_priority), \
|
|
125
|
+
patch.object(OpenModal, "_get_impact_descriptions", return_value="Test impact"):
|
|
126
|
+
blocks = OpenModal._build_response_type_blocks(open_incident_context)
|
|
127
|
+
assert len(blocks) == 1
|
|
128
|
+
first_block = blocks[0]
|
|
129
|
+
assert isinstance(first_block, ContextBlock)
|
|
113
130
|
|
|
114
131
|
|
|
115
132
|
@pytest.mark.django_db
|
|
File without changes
|
{firefighter_incident-0.0.9.dist-info → firefighter_incident-0.0.11.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{firefighter_incident-0.0.9.dist-info → firefighter_incident-0.0.11.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|