# What this PR does Adds `important` version of `Round-robin` escalation step <img width="1090" alt="Screenshot 2025-01-20 at 11 18 54" src="https://github.com/user-attachments/assets/add6f9e8-fc6c-40a8-a177-d727cc385651" /> ## Which issue(s) this PR closes Related to https://github.com/grafana/oncall/issues/1184 ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes.
328 lines
14 KiB
Python
328 lines
14 KiB
Python
from datetime import timedelta
|
|
|
|
from django.utils.functional import cached_property
|
|
from rest_framework import fields, serializers
|
|
|
|
from apps.alerts.models import EscalationChain, EscalationPolicy
|
|
from apps.alerts.utils import is_declare_incident_step_enabled
|
|
from apps.schedules.models import OnCallSchedule
|
|
from apps.slack.models import SlackUserGroup
|
|
from apps.user_management.models import Team, User
|
|
from apps.webhooks.models import Webhook
|
|
from common.api_helpers.custom_fields import (
|
|
DurationSecondsField,
|
|
OrganizationFilteredPrimaryKeyRelatedField,
|
|
UsersFilteredByOrganizationField,
|
|
)
|
|
from common.api_helpers.exceptions import BadRequest
|
|
from common.api_helpers.mixins import EagerLoadingMixin
|
|
from common.ordered_model.serializer import OrderedModelSerializer
|
|
|
|
|
|
class EscalationPolicyTypeField(fields.CharField):
|
|
def to_representation(self, value):
|
|
return EscalationPolicy.PUBLIC_STEP_CHOICES_MAP[value]
|
|
|
|
def to_internal_value(self, data):
|
|
try:
|
|
step_type = [
|
|
key
|
|
for key, value in EscalationPolicy.PUBLIC_STEP_CHOICES_MAP.items()
|
|
if value == data and key in EscalationPolicy.PUBLIC_STEP_CHOICES
|
|
][0]
|
|
except IndexError:
|
|
raise BadRequest(detail="Invalid escalation step type")
|
|
if step_type not in EscalationPolicy.PUBLIC_STEP_CHOICES:
|
|
raise BadRequest(detail="Invalid escalation step type")
|
|
return step_type
|
|
|
|
|
|
class EscalationPolicySerializer(EagerLoadingMixin, OrderedModelSerializer):
|
|
id = serializers.CharField(read_only=True, source="public_primary_key")
|
|
escalation_chain_id = OrganizationFilteredPrimaryKeyRelatedField(
|
|
queryset=EscalationChain.objects, source="escalation_chain"
|
|
)
|
|
type = EscalationPolicyTypeField(source="step")
|
|
|
|
duration = DurationSecondsField(
|
|
required=False,
|
|
source="wait_delay",
|
|
allow_null=True,
|
|
min_value=timedelta(minutes=1),
|
|
max_value=timedelta(hours=24),
|
|
)
|
|
|
|
persons_to_notify = UsersFilteredByOrganizationField(
|
|
queryset=User.objects,
|
|
required=False,
|
|
source="notify_to_users_queue",
|
|
)
|
|
team_to_notify = OrganizationFilteredPrimaryKeyRelatedField(
|
|
queryset=Team.objects,
|
|
required=False,
|
|
source="notify_to_team_members",
|
|
)
|
|
persons_to_notify_next_each_time = UsersFilteredByOrganizationField(
|
|
queryset=User.objects,
|
|
required=False,
|
|
source="notify_to_users_queue",
|
|
)
|
|
notify_on_call_from_schedule = OrganizationFilteredPrimaryKeyRelatedField(
|
|
queryset=OnCallSchedule.objects, required=False, source="notify_schedule"
|
|
)
|
|
group_to_notify = OrganizationFilteredPrimaryKeyRelatedField(
|
|
queryset=SlackUserGroup.objects,
|
|
required=False,
|
|
source="notify_to_group",
|
|
filter_field="slack_team_identity__organizations",
|
|
)
|
|
action_to_trigger = OrganizationFilteredPrimaryKeyRelatedField(
|
|
queryset=Webhook.objects,
|
|
required=False,
|
|
source="custom_webhook",
|
|
)
|
|
severity = serializers.CharField(required=False)
|
|
important = serializers.BooleanField(required=False)
|
|
|
|
TIME_FORMAT = "%H:%M:%SZ"
|
|
notify_if_time_from = serializers.TimeField(
|
|
required=False, source="from_time", format=TIME_FORMAT, input_formats=[TIME_FORMAT]
|
|
)
|
|
notify_if_time_to = serializers.TimeField(
|
|
required=False, source="to_time", format=TIME_FORMAT, input_formats=[TIME_FORMAT]
|
|
)
|
|
|
|
class Meta:
|
|
model = EscalationPolicy
|
|
fields = OrderedModelSerializer.Meta.fields + [
|
|
"id",
|
|
"escalation_chain_id",
|
|
"type",
|
|
"duration",
|
|
"important",
|
|
"action_to_trigger",
|
|
"persons_to_notify",
|
|
"team_to_notify",
|
|
"persons_to_notify_next_each_time",
|
|
"notify_on_call_from_schedule",
|
|
"group_to_notify",
|
|
"action_to_trigger",
|
|
"notify_if_time_from",
|
|
"notify_if_time_to",
|
|
"num_alerts_in_window",
|
|
"num_minutes_in_window",
|
|
"severity",
|
|
]
|
|
|
|
PREFETCH_RELATED = ["notify_to_users_queue"]
|
|
SELECT_RELATED = [
|
|
"custom_webhook",
|
|
"escalation_chain",
|
|
"notify_schedule",
|
|
"notify_to_group",
|
|
"notify_to_team_members",
|
|
]
|
|
|
|
@cached_property
|
|
def escalation_chain(self):
|
|
if self.instance is not None:
|
|
escalation_chain = self.instance.escalation_chain
|
|
else:
|
|
escalation_chain = EscalationChain.objects.get(public_primary_key=self.initial_data["escalation_chain_id"])
|
|
return escalation_chain
|
|
|
|
def validate_type(self, step_type):
|
|
organization = self.context["request"].auth.organization
|
|
|
|
if step_type == EscalationPolicy.STEP_FINAL_NOTIFYALL and organization.slack_team_identity is None:
|
|
raise BadRequest(detail="Invalid escalation step type: step is Slack-specific")
|
|
|
|
if step_type == EscalationPolicy.STEP_DECLARE_INCIDENT and not is_declare_incident_step_enabled(organization):
|
|
raise BadRequest("Invalid escalation step type: step is not enabled")
|
|
|
|
return step_type
|
|
|
|
def create(self, validated_data):
|
|
validated_data = self._correct_validated_data(validated_data)
|
|
return super().create(validated_data)
|
|
|
|
def to_representation(self, instance):
|
|
step = instance.step
|
|
result = super().to_representation(instance)
|
|
result = self._get_field_to_represent(step, result)
|
|
if "duration" in result and result["duration"] is not None:
|
|
result["duration"] = int(float(result["duration"]))
|
|
return result
|
|
|
|
def to_internal_value(self, data):
|
|
if data.get("persons_to_notify", []) is None: # terraform case
|
|
data["persons_to_notify"] = []
|
|
if data.get("persons_to_notify_next_each_time", []) is None: # terraform case
|
|
data["persons_to_notify_next_each_time"] = []
|
|
return super().to_internal_value(data)
|
|
|
|
def _get_field_to_represent(self, step, result):
|
|
fields_to_remove = [
|
|
"duration",
|
|
"team_to_notify",
|
|
"persons_to_notify",
|
|
"persons_to_notify_next_each_time",
|
|
"notify_on_call_from_schedule",
|
|
"group_to_notify",
|
|
"important",
|
|
"action_to_trigger",
|
|
"notify_if_time_from",
|
|
"notify_if_time_to",
|
|
"num_alerts_in_window",
|
|
"num_minutes_in_window",
|
|
"severity",
|
|
]
|
|
if step == EscalationPolicy.STEP_WAIT:
|
|
fields_to_remove.remove("duration")
|
|
elif step in [EscalationPolicy.STEP_NOTIFY_SCHEDULE, EscalationPolicy.STEP_NOTIFY_SCHEDULE_IMPORTANT]:
|
|
fields_to_remove.remove("notify_on_call_from_schedule")
|
|
elif step in [
|
|
EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS,
|
|
EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS_IMPORTANT,
|
|
]:
|
|
fields_to_remove.remove("persons_to_notify")
|
|
elif step in [
|
|
EscalationPolicy.STEP_NOTIFY_TEAM_MEMBERS,
|
|
EscalationPolicy.STEP_NOTIFY_TEAM_MEMBERS_IMPORTANT,
|
|
]:
|
|
fields_to_remove.remove("team_to_notify")
|
|
elif step in [
|
|
EscalationPolicy.STEP_NOTIFY_USERS_QUEUE,
|
|
EscalationPolicy.STEP_NOTIFY_USERS_QUEUE_IMPORTANT,
|
|
]:
|
|
fields_to_remove.remove("persons_to_notify_next_each_time")
|
|
elif step in [EscalationPolicy.STEP_NOTIFY_GROUP, EscalationPolicy.STEP_NOTIFY_GROUP_IMPORTANT]:
|
|
fields_to_remove.remove("group_to_notify")
|
|
elif step == EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK:
|
|
fields_to_remove.remove("action_to_trigger")
|
|
elif step == EscalationPolicy.STEP_NOTIFY_IF_TIME:
|
|
fields_to_remove.remove("notify_if_time_from")
|
|
fields_to_remove.remove("notify_if_time_to")
|
|
elif step == EscalationPolicy.STEP_NOTIFY_IF_NUM_ALERTS_IN_TIME_WINDOW:
|
|
fields_to_remove.remove("num_alerts_in_window")
|
|
fields_to_remove.remove("num_minutes_in_window")
|
|
elif step == EscalationPolicy.STEP_DECLARE_INCIDENT:
|
|
fields_to_remove.remove("severity")
|
|
|
|
if (
|
|
step in EscalationPolicy.DEFAULT_TO_IMPORTANT_STEP_MAPPING
|
|
or step in EscalationPolicy.DEFAULT_TO_IMPORTANT_STEP_MAPPING.values()
|
|
):
|
|
fields_to_remove.remove("important")
|
|
result["important"] = step not in EscalationPolicy.DEFAULT_TO_IMPORTANT_STEP_MAPPING
|
|
for field in fields_to_remove:
|
|
result.pop(field, None)
|
|
return result
|
|
|
|
def _correct_validated_data(self, validated_data):
|
|
validated_data_fields_to_remove = [
|
|
"notify_to_users_queue",
|
|
"wait_delay",
|
|
"notify_schedule",
|
|
"notify_to_group",
|
|
"notify_to_team_members",
|
|
"custom_webhook",
|
|
"from_time",
|
|
"to_time",
|
|
"num_alerts_in_window",
|
|
"num_minutes_in_window",
|
|
"severity",
|
|
]
|
|
step = validated_data.get("step")
|
|
important = validated_data.pop("important", None)
|
|
|
|
if step == EscalationPolicy._DEPRECATED_STEP_TRIGGER_CUSTOM_BUTTON and validated_data.get("custom_webhook"):
|
|
# migrate step to webhook
|
|
step = validated_data["step"] = EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK
|
|
|
|
if step in [EscalationPolicy.STEP_NOTIFY_SCHEDULE, EscalationPolicy.STEP_NOTIFY_SCHEDULE_IMPORTANT]:
|
|
validated_data_fields_to_remove.remove("notify_schedule")
|
|
elif step == EscalationPolicy.STEP_WAIT:
|
|
validated_data_fields_to_remove.remove("wait_delay")
|
|
elif step in [
|
|
EscalationPolicy.STEP_NOTIFY_USERS_QUEUE,
|
|
EscalationPolicy.STEP_NOTIFY_USERS_QUEUE_IMPORTANT,
|
|
EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS,
|
|
EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS_IMPORTANT,
|
|
]:
|
|
validated_data_fields_to_remove.remove("notify_to_users_queue")
|
|
elif step in [EscalationPolicy.STEP_NOTIFY_GROUP, EscalationPolicy.STEP_NOTIFY_GROUP_IMPORTANT]:
|
|
validated_data_fields_to_remove.remove("notify_to_group")
|
|
elif step in [EscalationPolicy.STEP_NOTIFY_TEAM_MEMBERS, EscalationPolicy.STEP_NOTIFY_TEAM_MEMBERS_IMPORTANT]:
|
|
validated_data_fields_to_remove.remove("notify_to_team_members")
|
|
elif step == EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK:
|
|
validated_data_fields_to_remove.remove("custom_webhook")
|
|
elif step == EscalationPolicy.STEP_NOTIFY_IF_TIME:
|
|
validated_data_fields_to_remove.remove("from_time")
|
|
validated_data_fields_to_remove.remove("to_time")
|
|
elif step == EscalationPolicy.STEP_NOTIFY_IF_NUM_ALERTS_IN_TIME_WINDOW:
|
|
validated_data_fields_to_remove.remove("num_alerts_in_window")
|
|
validated_data_fields_to_remove.remove("num_minutes_in_window")
|
|
elif step == EscalationPolicy.STEP_DECLARE_INCIDENT:
|
|
validated_data_fields_to_remove.remove("severity")
|
|
|
|
for field in validated_data_fields_to_remove:
|
|
validated_data.pop(field, None)
|
|
|
|
if step in EscalationPolicy.DEFAULT_TO_IMPORTANT_STEP_MAPPING and important:
|
|
validated_data["step"] = EscalationPolicy.DEFAULT_TO_IMPORTANT_STEP_MAPPING[step]
|
|
elif step in EscalationPolicy.DEFAULT_TO_IMPORTANT_STEP_MAPPING.values() and important is False:
|
|
validated_data["step"] = [
|
|
key for key, value in EscalationPolicy.DEFAULT_TO_IMPORTANT_STEP_MAPPING.items() if value == step
|
|
][0]
|
|
return validated_data
|
|
|
|
|
|
class EscalationPolicyUpdateSerializer(EscalationPolicySerializer):
|
|
escalation_chain_id = OrganizationFilteredPrimaryKeyRelatedField(read_only=True, source="escalation_chain")
|
|
type = EscalationPolicyTypeField(required=False, source="step", allow_null=True)
|
|
|
|
class Meta(EscalationPolicySerializer.Meta):
|
|
read_only_fields = ["route_id"]
|
|
|
|
def update(self, instance, validated_data):
|
|
if "step" in validated_data:
|
|
step = validated_data["step"]
|
|
else:
|
|
step = instance.step
|
|
|
|
validated_data["step"] = step
|
|
validated_data = self._correct_validated_data(validated_data)
|
|
|
|
if step != instance.step:
|
|
if step is not None:
|
|
if step not in [EscalationPolicy.STEP_NOTIFY_SCHEDULE, EscalationPolicy.STEP_NOTIFY_SCHEDULE_IMPORTANT]:
|
|
instance.notify_schedule = None
|
|
if step != EscalationPolicy.STEP_WAIT:
|
|
instance.wait_delay = None
|
|
if step not in [
|
|
EscalationPolicy.STEP_NOTIFY_USERS_QUEUE,
|
|
EscalationPolicy.STEP_NOTIFY_USERS_QUEUE_IMPORTANT,
|
|
EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS,
|
|
EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS_IMPORTANT,
|
|
]:
|
|
instance.notify_to_users_queue.clear()
|
|
if step not in [
|
|
EscalationPolicy.STEP_NOTIFY_TEAM_MEMBERS,
|
|
EscalationPolicy.STEP_NOTIFY_TEAM_MEMBERS_IMPORTANT,
|
|
]:
|
|
instance.notify_to_team_members = None
|
|
if step not in [EscalationPolicy.STEP_NOTIFY_GROUP, EscalationPolicy.STEP_NOTIFY_GROUP_IMPORTANT]:
|
|
instance.notify_to_group = None
|
|
if step != EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK:
|
|
instance.custom_webhook = None
|
|
if step != EscalationPolicy.STEP_NOTIFY_IF_TIME:
|
|
instance.from_time = None
|
|
instance.to_time = None
|
|
if step != EscalationPolicy.STEP_NOTIFY_IF_NUM_ALERTS_IN_TIME_WINDOW:
|
|
instance.num_alerts_in_window = None
|
|
instance.num_minutes_in_window = None
|
|
if step != EscalationPolicy.STEP_DECLARE_INCIDENT:
|
|
instance.severity = None
|
|
|
|
return super().update(instance, validated_data)
|