oncall-engine/engine/apps/api/serializers/user_notification_policy.py
Vadim Stepanov 602ed535e3
Fix duplicate orders on routes and escalation policies (#2568)
# What this PR does

Fix duplicate `order` values for models `EscalationPolicy` and
`ChannelFilter` using the same approach as
https://github.com/grafana/oncall/pull/2278.

- Make internal API action `move_to_position` a part of
[OrderedModelViewSet](https://github.com/grafana/oncall/pull/2568/files#diff-eb62521ccbcb072d1bd2156adeadae3b5895bce6d0d54432a23db3948b0ada54R11-R34),
so all ordered model views use the same logic.
- Make public API serializers for ordered models inherit from
[OrderedModelSerializer](https://github.com/grafana/oncall/pull/2568/files#diff-d749f94af5a49adaf5074325cdfad10ddd5a52dbfd78b49582867ebb9c92fae5R6-R38),
so ordered model views are consistent with each other in public API.
- Remove `order` from plugin fronted, since it's not being used
anywhere. The frontend uses step indices & `move_to_position` action
instead.
- Make escalation snapshot use step indices instead of orders, since
orders are not guaranteed to be sequential (+fix a minor off-by-one bug)

## Which issue(s) this PR fixes

https://github.com/grafana/oncall-private/issues/1680

## 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] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not
required)
2023-07-18 17:17:53 +00:00

117 lines
4.7 KiB
Python

import time
from datetime import timedelta
from rest_framework import serializers
from apps.base.models import UserNotificationPolicy
from apps.base.models.user_notification_policy import NotificationChannelAPIOptions
from apps.user_management.models import User
from common.api_helpers.custom_fields import OrganizationFilteredPrimaryKeyRelatedField
from common.api_helpers.exceptions import Forbidden
from common.api_helpers.mixins import EagerLoadingMixin
# This serializer should not be user directly
class UserNotificationPolicyBaseSerializer(EagerLoadingMixin, serializers.ModelSerializer):
id = serializers.CharField(read_only=True, source="public_primary_key")
notify_by = serializers.ChoiceField(
read_only=False,
required=False,
default=UserNotificationPolicy.NotificationChannel.SLACK,
choices=NotificationChannelAPIOptions.AVAILABLE_FOR_USE,
)
step = serializers.ChoiceField(
read_only=False,
required=False,
default=UserNotificationPolicy.Step.NOTIFY,
choices=UserNotificationPolicy.Step.choices,
)
SELECT_RELATED = [
"user",
]
class Meta:
model = UserNotificationPolicy
fields = ["id", "step", "notify_by", "wait_delay", "important", "user"]
# Field "order" is not consumed by the plugin frontend, but is used by the mobile app
# TODO: remove this field when the mobile app is updated
fields += ["order"]
read_only_fields = ["order"]
def to_internal_value(self, data):
if data.get("wait_delay", None):
try:
time.strptime(data["wait_delay"], "%H:%M:%S")
except ValueError:
try:
data["wait_delay"] = str(timedelta(seconds=float(data["wait_delay"])))
except ValueError:
raise serializers.ValidationError("Invalid wait delay format")
data = self._notify_by_to_internal_value(data)
return super().to_internal_value(data)
def to_representation(self, instance):
result = super().to_representation(instance)
result = self._notify_by_to_representation(instance, result)
return result
# _notify_by_to_internal_value and _notify_by_to_representation are exists because of in EscalationPolicy model
# notify_by field has default value NotificationChannel.SLACK and not nullable
# We don't want any notify_by value in response if step != Step.NOTIFY
def _notify_by_to_internal_value(self, data):
if not data.get("notify_by", None):
data["notify_by"] = UserNotificationPolicy.NotificationChannel.SLACK
return data
def _notify_by_to_representation(self, instance, result):
if instance.step != UserNotificationPolicy.Step.NOTIFY:
result["notify_by"] = None
return result
class UserNotificationPolicySerializer(UserNotificationPolicyBaseSerializer):
user = OrganizationFilteredPrimaryKeyRelatedField(
queryset=User.objects,
required=False,
allow_null=True,
many=False,
display_func=lambda instance: instance.username,
)
notify_by = serializers.ChoiceField(
choices=NotificationChannelAPIOptions.AVAILABLE_FOR_USE,
default=NotificationChannelAPIOptions.DEFAULT_NOTIFICATION_CHANNEL,
)
def create(self, validated_data):
user = validated_data.get("user") or self.context["request"].user
organization = self.context["request"].auth.organization
self_or_admin = user.self_or_admin(user_to_check=self.context["request"].user, organization=organization)
if not self_or_admin:
raise Forbidden()
instance = UserNotificationPolicy.objects.create(**validated_data)
return instance
class UserNotificationPolicyUpdateSerializer(UserNotificationPolicyBaseSerializer):
user = OrganizationFilteredPrimaryKeyRelatedField(
many=False,
read_only=True,
display_func=lambda instance: instance.username,
)
class Meta(UserNotificationPolicyBaseSerializer.Meta):
read_only_fields = UserNotificationPolicyBaseSerializer.Meta.read_only_fields + ["user", "important"]
def update(self, instance, validated_data):
self_or_admin = instance.user.self_or_admin(
user_to_check=self.context["request"].user, organization=self.context["request"].user.organization
)
if not self_or_admin:
raise Forbidden()
if validated_data.get("step") == UserNotificationPolicy.Step.WAIT and not validated_data.get("wait_delay"):
validated_data["wait_delay"] = UserNotificationPolicy.FIVE_MINUTES
return super().update(instance, validated_data)