Skip to content

Commit

Permalink
Add edit command to update an incident
Browse files Browse the repository at this point in the history
  • Loading branch information
jennafauconnier committed Aug 29, 2024
1 parent 7fbaea4 commit fac7d99
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 13 deletions.
33 changes: 33 additions & 0 deletions src/firefighter/incidents/forms/edit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from __future__ import annotations

from django import forms

from firefighter.incidents.models import Environment


def initial_environments() -> Environment:
return Environment.objects.get(default=True)


class EditMetaForm(forms.Form):
title = forms.CharField(
label="Title",
max_length=128,
min_length=10,
widget=forms.TextInput(attrs={"placeholder": "What's going on?"}),
)
description = forms.CharField(
label="Summary",
widget=forms.Textarea(
attrs={
"placeholder": "Help people responding to the incident. This will be posted to #tech-incidents and on our internal status page.\nThis description can be edited later."
}
),
min_length=10,
max_length=1200,
)
environment = forms.ModelChoiceField(
label="Environment",
queryset=Environment.objects.all(),
initial=initial_environments,
)
34 changes: 21 additions & 13 deletions src/firefighter/incidents/models/incident.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,21 +335,27 @@ def can_be_closed(self) -> tuple[bool, list[tuple[str, str]]]:
return True, []
if self.needs_postmortem:
if self.status.value != IncidentStatus.POST_MORTEM:
cant_closed_reasons.append((
"STATUS_NOT_POST_MORTEM",
f"Incident is not in PostMortem status, and needs one because of its priority and environment ({self.priority.name}/{self.environment.value}).",
))
cant_closed_reasons.append(
(
"STATUS_NOT_POST_MORTEM",
f"Incident is not in PostMortem status, and needs one because of its priority and environment ({self.priority.name}/{self.environment.value}).",
)
)
elif self.status.value < IncidentStatus.FIXED:
cant_closed_reasons.append((
"STATUS_NOT_MITIGATED",
f"Incident is not in {IncidentStatus.FIXED.label} status (currently {self.status.label}).",
))
cant_closed_reasons.append(
(
"STATUS_NOT_MITIGATED",
f"Incident is not in {IncidentStatus.FIXED.label} status (currently {self.status.label}).",
)
)
missing_milestones = self.missing_milestones()
if len(missing_milestones) > 0:
cant_closed_reasons.append((
"MISSING_REQUIRED_KEY_EVENTS",
f"Missing key events: {', '.join(missing_milestones)}",
))
cant_closed_reasons.append(
(
"MISSING_REQUIRED_KEY_EVENTS",
f"Missing key events: {', '.join(missing_milestones)}",
)
)

if len(cant_closed_reasons) > 0:
return False, cant_closed_reasons
Expand Down Expand Up @@ -534,7 +540,7 @@ def update_roles(

return incident_update

def create_incident_update(
def create_incident_update( # noqa: PLR0913
self: Incident,
message: str | None = None,
status: int | None = None,
Expand All @@ -544,6 +550,7 @@ def create_incident_update(
event_type: str | None = None,
title: str | None = None,
description: str | None = None,
environment_id: str | None = None,
event_ts: datetime | None = None,
) -> IncidentUpdate:
updated_fields: list[str] = []
Expand All @@ -560,6 +567,7 @@ def _update_incident_field(
_update_incident_field(self, "component_id", component_id, updated_fields)
_update_incident_field(self, "title", title, updated_fields)
_update_incident_field(self, "description", description, updated_fields)
_update_incident_field(self, "environment_id", environment_id, updated_fields)

old_priority = self.priority if priority_id is not None else None

Expand Down
3 changes: 3 additions & 0 deletions src/firefighter/slack/views/events/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from firefighter.slack.views.modals import (
modal_close,
modal_dowgrade_workflow,
modal_edit,
modal_open,
modal_postmortem,
modal_send_sos,
Expand Down Expand Up @@ -106,6 +107,8 @@ def manage_incident(ack: Ack, respond: Respond, body: dict[str, Any]) -> None:
modal_open.open_modal_aio(ack, body)
elif command == "update":
modal_update.open_modal_aio(ack, body)
elif command == "edit":
modal_edit.open_modal_aio(ack, body)
elif command == "close":
modal_close.open_modal_aio(ack=ack, body=body)
elif command == "status":
Expand Down
2 changes: 2 additions & 0 deletions src/firefighter/slack/views/modals/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
DowngradeWorkflowModal,
modal_dowgrade_workflow,
)
from firefighter.slack.views.modals.edit import EditMetaModal, modal_edit
from firefighter.slack.views.modals.key_event_message import ( # XXX(dugab) move and rename (not a modal but a surface...)
KeyEvents,
)
Expand Down Expand Up @@ -39,6 +40,7 @@

selectable_modals: list[type[SlackModal]] = [
UpdateModal,
EditMetaModal,
UpdateRolesModal,
OnCallModal,
CloseModal,
Expand Down
117 changes: 117 additions & 0 deletions src/firefighter/slack/views/modals/edit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Any

from slack_sdk.models.views import View

from firefighter.incidents.forms.edit import EditMetaForm
from firefighter.slack.views.modals.base_modal.base import ModalForm

if TYPE_CHECKING:
from slack_bolt.context.ack.ack import Ack

from firefighter.incidents.models.incident import Incident
from firefighter.incidents.models.user import User
from firefighter.slack.views.modals.base_modal.form_utils import (
SlackFormAttributesDict,
)

logger = logging.getLogger(__name__)


class EditMetaFormSlack(EditMetaForm):
slack_fields: SlackFormAttributesDict = {
"title": {
"input": {
"multiline": False,
"placeholder": "Short, punchy description of what's happening.",
},
"block": {"hint": None},
},
"description": {
"input": {
"multiline": True,
"placeholder": "Help people responding to the incident. This will be posted to #tech-incidents and on our internal status page.\nThis description can be edited later.",
},
"block": {"hint": None},
},
"environment": {
"input": {
"placeholder": "Select an environment",
},
"widget": {
"label_from_instance": lambda obj: f"{obj.value} - {obj.description}",
},
},
}


class EditMetaModal(ModalForm[EditMetaFormSlack]):
open_action: str = "open_modal_incident_edit"
update_action: str = "update_modal_incident_edit"
push_action: str = "push_modal_incident_edit"
open_shortcut = "modal_edit"
callback_id: str = "incident_edit_incident"

form_class = EditMetaFormSlack

def build_modal_fn(self, incident: Incident, **kwargs: Any) -> View:
blocks = self.get_form_class()(
initial={
"title": incident.title,
"description": incident.description,
"environment": incident.environment,
},
).slack_blocks()

return View(
type="modal",
title=f"Update incident #{incident.id}"[:24],
submit="Update incident"[:24],
callback_id=self.callback_id,
private_metadata=str(incident.id),
blocks=blocks,
)

def handle_modal_fn( # type: ignore
self, ack: Ack, body: dict[str, Any], incident: Incident, user: User
):

slack_form = self.handle_form_errors(
ack,
body,
forms_kwargs={
"initial": {
"title": incident.title,
"description": incident.description,
"environment": incident.environment,
}
},
)
if slack_form is None:
return
form: EditMetaFormSlack = slack_form.form
if len(form.cleaned_data) == 0:
# XXX We should have a prompt for empty forms

return
update_kwargs: dict[str, Any] = {}
for changed_key in form.changed_data:
if changed_key == "environment":
update_kwargs[f"{changed_key}_id"] = form.cleaned_data[changed_key].id
if changed_key in {"description", "title"}:
update_kwargs[changed_key] = form.cleaned_data[changed_key]
if len(update_kwargs) == 0:
logger.warning("No update to incident status")
return
self._trigger_incident_workflow(incident, user, **update_kwargs)

@staticmethod
def _trigger_incident_workflow(
incident: Incident, user: User, **kwargs: Any
) -> None:
incident.create_incident_update(created_by=user, **kwargs)


modal_edit = EditMetaModal()
5 changes: 5 additions & 0 deletions src/firefighter/slack/views/modals/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from firefighter.slack.views.modals.base_modal.mixins import (
IncidentSelectableModalMixin,
)
from firefighter.slack.views.modals.edit import EditMetaModal
from firefighter.slack.views.modals.update_roles import UpdateRolesModal
from firefighter.slack.views.modals.update_status import UpdateStatusModal

Expand Down Expand Up @@ -46,6 +47,10 @@ def build_modal_fn(self, incident: Incident, **kwargs: Any) -> View:
text="Update roles",
action_id=UpdateRolesModal.push_action,
),
ButtonElement(
text="Edit meta",
action_id=EditMetaModal.push_action,
),
],
)
],
Expand Down

0 comments on commit fac7d99

Please sign in to comment.