oncall-engine/engine/apps/public_api/serializers/escalation_policies.py
Julia Artyukhina 84411b7250
Add important version of round-robin escalation step (#5418)
# 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.
2025-01-21 16:29:36 +00:00

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)