Add is_default fields to templates, remove WritableSerialiserMethodFi… (#1759)

…eld, refactor fields

# What this PR does

## Which issue(s) this PR fixes

## Checklist

- [ ] Unit, integration, and e2e (if applicable) tests updated
- [ ] Documentation added (or `pr:no public docs` PR label added if not
required)
- [ ] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not
required)
This commit is contained in:
Ildar Iskhakov 2023-04-18 12:44:51 +08:00 committed by GitHub
parent 5da5b8d430
commit 61dced5bd9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 69 additions and 347 deletions

View file

@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Helm chart: add the option to use a helm hook for the migration job ([1386](https://github.com/grafana/oncall/pull/1386))
- Add endpoints to start and stop maintenance in alert receive channel private api ([1755](https://github.com/grafana/oncall/pull/1755))
- Send demo alert with dynamic payload and get demo payload example on private api ([1700](https://github.com/grafana/oncall/pull/1700))
- Add is_default fields to templates, remove WritableSerialiserMethodField ([1759](https://github.com/grafana/oncall/pull/1759))
- Allow use of dynamic payloads in alert receive channels preview template in private api ([1756](https://github.com/grafana/oncall/pull/1756))
## v1.2.11 (2023-04-14)

View file

@ -1,22 +1,19 @@
from collections import OrderedDict
from collections.abc import Mapping
from django.apps import apps
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ValidationError as DjangoValidationError
from django.template.loader import render_to_string
from django.utils import timezone
from jinja2 import TemplateSyntaxError
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.fields import SerializerMethodField, SkipField, get_error_detail, set_value
from rest_framework.settings import api_settings
from rest_framework.fields import SerializerMethodField, set_value
from apps.alerts.grafana_alerting_sync_manager.grafana_alerting_sync import GrafanaAlertingSyncManager
from apps.alerts.models import AlertReceiveChannel
from apps.base.messaging import get_messaging_backends
from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField, WritableSerializerMethodField
from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField
from common.api_helpers.exceptions import BadRequest
from common.api_helpers.mixins import APPEARANCE_TEMPLATE_NAMES, EagerLoadingMixin
from common.api_helpers.utils import CurrentTeamDefault
@ -197,96 +194,23 @@ class FilterAlertReceiveChannelSerializer(serializers.ModelSerializer):
class AlertReceiveChannelTemplatesSerializer(EagerLoadingMixin, serializers.ModelSerializer):
id = serializers.CharField(read_only=True, source="public_primary_key")
slack_title_template = WritableSerializerMethodField(
allow_null=True,
deserializer_field=serializers.CharField(),
validators=[valid_jinja_template_for_serializer_method_field],
required=False,
)
slack_message_template = WritableSerializerMethodField(
allow_null=True,
deserializer_field=serializers.CharField(),
validators=[valid_jinja_template_for_serializer_method_field],
required=False,
)
slack_image_url_template = WritableSerializerMethodField(
allow_null=True,
deserializer_field=serializers.CharField(),
validators=[valid_jinja_template_for_serializer_method_field],
required=False,
)
web_title_template = WritableSerializerMethodField(
allow_null=True,
deserializer_field=serializers.CharField(),
validators=[valid_jinja_template_for_serializer_method_field],
required=False,
)
web_message_template = WritableSerializerMethodField(
allow_null=True,
deserializer_field=serializers.CharField(),
validators=[valid_jinja_template_for_serializer_method_field],
required=False,
)
web_image_url_template = WritableSerializerMethodField(
allow_null=True,
deserializer_field=serializers.CharField(),
validators=[valid_jinja_template_for_serializer_method_field],
required=False,
)
sms_title_template = WritableSerializerMethodField(
allow_null=True,
deserializer_field=serializers.CharField(),
validators=[valid_jinja_template_for_serializer_method_field],
required=False,
)
phone_call_title_template = WritableSerializerMethodField(
allow_null=True,
deserializer_field=serializers.CharField(),
validators=[valid_jinja_template_for_serializer_method_field],
required=False,
)
telegram_title_template = WritableSerializerMethodField(
allow_null=True,
deserializer_field=serializers.CharField(),
validators=[valid_jinja_template_for_serializer_method_field],
required=False,
)
telegram_message_template = WritableSerializerMethodField(
allow_null=True,
deserializer_field=serializers.CharField(),
validators=[valid_jinja_template_for_serializer_method_field],
required=False,
)
telegram_image_url_template = WritableSerializerMethodField(
allow_null=True,
deserializer_field=serializers.CharField(),
validators=[valid_jinja_template_for_serializer_method_field],
required=False,
)
source_link_template = WritableSerializerMethodField(
allow_null=True,
deserializer_field=serializers.CharField(),
validators=[valid_jinja_template_for_serializer_method_field],
required=False,
)
grouping_id_template = WritableSerializerMethodField(
allow_null=True,
deserializer_field=serializers.CharField(),
validators=[valid_jinja_template_for_serializer_method_field],
required=False,
)
acknowledge_condition_template = WritableSerializerMethodField(
allow_null=True,
deserializer_field=serializers.CharField(),
validators=[valid_jinja_template_for_serializer_method_field],
required=False,
)
resolve_condition_template = WritableSerializerMethodField(
allow_null=True,
deserializer_field=serializers.CharField(),
validators=[valid_jinja_template_for_serializer_method_field],
required=False,
)
CORE_TEMPLATE_NAMES = [
"slack_title_template",
"slack_message_template",
"slack_image_url_template",
"web_title_template",
"web_message_template",
"web_image_url_template",
"telegram_title_template",
"telegram_message_template",
"telegram_image_url_template",
"sms_title_template",
"phone_call_title_template",
"source_link_template",
"grouping_id_template",
"resolve_condition_template",
"acknowledge_condition_template",
]
payload_example = SerializerMethodField()
@ -295,207 +219,10 @@ class AlertReceiveChannelTemplatesSerializer(EagerLoadingMixin, serializers.Mode
fields = [
"id",
"verbal_name",
"slack_title_template",
"slack_message_template",
"slack_image_url_template",
"sms_title_template",
"phone_call_title_template",
"web_title_template",
"web_message_template",
"web_image_url_template",
"telegram_title_template",
"telegram_message_template",
"telegram_image_url_template",
"source_link_template",
"grouping_id_template",
"resolve_condition_template",
"payload_example",
"acknowledge_condition_template",
]
extra_kwargs = {"integration": {"required": True}}
# MethodFields are used instead of relevant properties because of properties hit db on each instance in queryset
def get_slack_title_template(self, obj):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_SLACK_TITLE_TEMPLATE[obj.integration]
return obj.slack_title_template or default_template
def set_slack_title_template(self, value):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_SLACK_TITLE_TEMPLATE[self.instance.integration]
if default_template is None or default_template.strip() != value.strip():
self.instance.slack_title_template = value.strip()
elif default_template is not None and default_template.strip() == value.strip():
self.instance.slack_title_template = None
def get_slack_message_template(self, obj):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_SLACK_MESSAGE_TEMPLATE[obj.integration]
return obj.slack_message_template or default_template
def set_slack_message_template(self, value):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_SLACK_MESSAGE_TEMPLATE[self.instance.integration]
if default_template is None or default_template.strip() != value.strip():
self.instance.slack_message_template = value.strip()
elif default_template is not None and default_template.strip() == value.strip():
self.instance.slack_message_template = None
def get_slack_image_url_template(self, obj):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_SLACK_IMAGE_URL_TEMPLATE[obj.integration]
return obj.slack_image_url_template or default_template
def set_slack_image_url_template(self, value):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_SLACK_IMAGE_URL_TEMPLATE[
self.instance.integration
]
if default_template is None or default_template.strip() != value.strip():
self.instance.slack_image_url_template = value.strip()
elif default_template is not None and default_template.strip() == value.strip():
self.instance.slack_image_url_template = None
def get_sms_title_template(self, obj):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_SMS_TITLE_TEMPLATE[obj.integration]
return obj.sms_title_template or default_template
def set_sms_title_template(self, value):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_SMS_TITLE_TEMPLATE[self.instance.integration]
if default_template is None or default_template.strip() != value.strip():
self.instance.sms_title_template = value.strip()
elif default_template is not None and default_template.strip() == value.strip():
self.instance.sms_title_template = None
def get_phone_call_title_template(self, obj):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_PHONE_CALL_TITLE_TEMPLATE[obj.integration]
return obj.phone_call_title_template or default_template
def set_phone_call_title_template(self, value):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_PHONE_CALL_TITLE_TEMPLATE[
self.instance.integration
]
if default_template is None or default_template.strip() != value.strip():
self.instance.phone_call_title_template = value.strip()
elif default_template is not None and default_template.strip() == value.strip():
self.instance.phone_call_title_template = None
def get_web_title_template(self, obj):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_WEB_TITLE_TEMPLATE[obj.integration]
return obj.web_title_template or default_template
def set_web_title_template(self, value):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_WEB_TITLE_TEMPLATE[self.instance.integration]
if default_template is None or default_template.strip() != value.strip():
self.instance.web_title_template = value.strip()
elif default_template is not None and default_template.strip() == value.strip():
self.instance.web_title_template = None
self.instance.web_templates_modified_at = timezone.now()
def get_web_message_template(self, obj):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_WEB_MESSAGE_TEMPLATE[obj.integration]
return obj.web_message_template or default_template
def set_web_message_template(self, value):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_WEB_MESSAGE_TEMPLATE[self.instance.integration]
if default_template is None or default_template.strip() != value.strip():
self.instance.web_message_template = value.strip()
elif default_template is not None and default_template.strip() == value.strip():
self.instance.web_message_template = None
self.instance.web_templates_modified_at = timezone.now()
def get_web_image_url_template(self, obj):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_WEB_IMAGE_URL_TEMPLATE[obj.integration]
return obj.web_image_url_template or default_template
def set_web_image_url_template(self, value):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_WEB_IMAGE_URL_TEMPLATE[self.instance.integration]
if default_template is None or default_template.strip() != value.strip():
self.instance.web_image_url_template = value.strip()
elif default_template is not None and default_template.strip() == value.strip():
self.instance.web_image_url_template = None
self.instance.web_templates_modified_at = timezone.now()
def get_telegram_title_template(self, obj):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_TELEGRAM_TITLE_TEMPLATE[obj.integration]
return obj.telegram_title_template or default_template
def set_telegram_title_template(self, value):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_TELEGRAM_TITLE_TEMPLATE[self.instance.integration]
if default_template is None or default_template.strip() != value.strip():
self.instance.telegram_title_template = value.strip()
elif default_template is not None and default_template.strip() == value.strip():
self.instance.telegram_title_template = None
def get_telegram_message_template(self, obj):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_TELEGRAM_MESSAGE_TEMPLATE[obj.integration]
return obj.telegram_message_template or default_template
def set_telegram_message_template(self, value):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_TELEGRAM_MESSAGE_TEMPLATE[
self.instance.integration
]
if default_template is None or default_template.strip() != value.strip():
self.instance.telegram_message_template = value.strip()
elif default_template is not None and default_template.strip() == value.strip():
self.instance.telegram_message_template = None
def get_telegram_image_url_template(self, obj):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_TELEGRAM_IMAGE_URL_TEMPLATE[obj.integration]
return obj.telegram_image_url_template or default_template
def set_telegram_image_url_template(self, value):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_TELEGRAM_IMAGE_URL_TEMPLATE[
self.instance.integration
]
if default_template is None or default_template.strip() != value.strip():
self.instance.telegram_image_url_template = value.strip()
elif default_template is not None and default_template.strip() == value.strip():
self.instance.telegram_image_url_template = None
def get_source_link_template(self, obj):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_SOURCE_LINK_TEMPLATE[obj.integration]
return obj.source_link_template or default_template
def set_source_link_template(self, value):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_SOURCE_LINK_TEMPLATE[self.instance.integration]
if default_template is None or default_template.strip() != value.strip():
self.instance.source_link_template = value.strip()
elif default_template is not None and default_template.strip() == value.strip():
self.instance.source_link_template = None
def get_grouping_id_template(self, obj):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_GROUPING_ID_TEMPLATE[obj.integration]
return obj.grouping_id_template or default_template
def set_grouping_id_template(self, value):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_GROUPING_ID_TEMPLATE[self.instance.integration]
if default_template is None or default_template.strip() != value.strip():
self.instance.grouping_id_template = value.strip()
elif default_template is not None and default_template.strip() == value.strip():
self.instance.grouping_id_template = None
def get_acknowledge_condition_template(self, obj):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_ACKNOWLEDGE_CONDITION_TEMPLATE[obj.integration]
return obj.acknowledge_condition_template or default_template
def set_acknowledge_condition_template(self, value):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_ACKNOWLEDGE_CONDITION_TEMPLATE[
self.instance.integration
]
if default_template is None or default_template.strip() != value.strip():
self.instance.acknowledge_condition_template = value.strip()
elif default_template is not None and default_template.strip() == value.strip():
self.instance.acknowledge_condition_template = None
def get_resolve_condition_template(self, obj):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_RESOLVE_CONDITION_TEMPLATE[obj.integration]
return obj.resolve_condition_template or default_template
def set_resolve_condition_template(self, value):
default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_RESOLVE_CONDITION_TEMPLATE[
self.instance.integration
]
if default_template is None or default_template.strip() != value.strip():
self.instance.resolve_condition_template = value.strip()
elif default_template is not None and default_template.strip() == value.strip():
self.instance.resolve_condition_template = None
def get_payload_example(self, obj):
AlertGroup = apps.get_model("alerts", "AlertGroup")
if "alert_group_id" in self.context["request"].query_params:
@ -517,33 +244,15 @@ class AlertReceiveChannelTemplatesSerializer(EagerLoadingMixin, serializers.Mode
"""
Dict of native values <- Dict of primitive datatypes.
"""
if not isinstance(data, Mapping):
message = self.error_messages["invalid"].format(datatype=type(data).__name__)
raise ValidationError({api_settings.NON_FIELD_ERRORS_KEY: [message]}, code="invalid")
# First validate and save data from serializer fields
ret = super().to_internal_value(data)
ret = OrderedDict()
# Separately validate and save template fields we generate dynamically
errors = OrderedDict()
fields = self._writable_fields
for field in fields:
validate_method = getattr(self, "validate_" + field.field_name, None)
primitive_value = field.get_value(data)
try:
validated_value = field.run_validation(primitive_value)
if validate_method is not None:
validated_value = validate_method(validated_value)
except ValidationError as exc:
errors[field.field_name] = exc.detail
except DjangoValidationError as exc:
errors[field.field_name] = get_error_detail(exc)
except SkipField:
pass
else:
# Line because of which method is overriden
if validated_value is None and isinstance(field, WritableSerializerMethodField):
set_value(ret, [field.field_name], validated_value)
else:
set_value(ret, field.source_attrs, validated_value)
# handle updates for core templates
core_template_errors = self._handle_core_template_updates(data, ret)
errors.update(core_template_errors)
# handle updates for messaging backend templates
messaging_backend_errors = self._handle_messaging_backend_updates(data, ret)
@ -551,7 +260,6 @@ class AlertReceiveChannelTemplatesSerializer(EagerLoadingMixin, serializers.Mode
if errors:
raise ValidationError(errors)
return ret
def _handle_messaging_backend_updates(self, data, ret):
@ -586,10 +294,33 @@ class AlertReceiveChannelTemplatesSerializer(EagerLoadingMixin, serializers.Mode
return errors
def _handle_core_template_updates(self, data, ret):
"""Update core templates if needed."""
errors = {}
core_template_names = self.CORE_TEMPLATE_NAMES
for field_name in core_template_names:
value = data.get(field_name)
validator = jinja_template_env.from_string
if value is not None:
try:
if value:
validator(value)
except TemplateSyntaxError:
errors[field_name] = "invalid template"
except DjangoValidationError:
errors[field_name] = "invalid URL"
set_value(ret, [field_name], value)
return errors
def to_representation(self, obj):
ret = super().to_representation(obj)
ret = self._get_templates_to_show(ret)
core_templates = self._get_core_templates(obj)
ret.update(core_templates)
# include messaging backend templates
additional_templates = self._get_messaging_backend_templates(obj)
ret.update(additional_templates)
@ -627,10 +358,26 @@ class AlertReceiveChannelTemplatesSerializer(EagerLoadingMixin, serializers.Mode
continue
for field in backend.template_fields:
value = None
is_default = False
if obj.messaging_backends_templates:
value = obj.messaging_backends_templates.get(backend_id, {}).get(field)
if not value:
value = obj.get_default_template_attribute(backend_id, field)
is_default = True
field_name = f"{backend.slug}_{field}_template"
templates[field_name] = value
templates[f"{field_name}_is_default"] = is_default
return templates
def _get_core_templates(self, obj):
core_templates = {}
core_template_names = self.CORE_TEMPLATE_NAMES
for template_name in core_template_names:
template_value = getattr(obj, template_name)
defaults = getattr(obj, f"INTEGRATION_TO_DEFAULT_{template_name.upper()}", {})
default_template_value = defaults.get(obj.integration)
core_templates[template_name] = template_value or default_template_value
core_templates[f"{template_name}_is_default"] = not bool(template_value)
return core_templates

View file

@ -376,4 +376,7 @@ def test_update_alert_receive_channel_templates(
# check if updated templates are applied
updated_templates_data = response.json()
for template_name, prev_template_value in existing_templates_data.items():
assert updated_templates_data[template_name] == template_update_func(prev_template_value)
if template_name.endswith("_is_default"):
assert updated_templates_data[template_name] is False
else:
assert updated_templates_data[template_name] == template_update_func(prev_template_value)

View file

@ -101,35 +101,6 @@ class UsersFilteredByOrganizationField(serializers.Field):
return queryset.filter(organization=request.user.organization, public_primary_key__in=data).distinct()
class WritableSerializerMethodField(serializers.SerializerMethodField):
"""
Please, NEVER use this field.
It was a mistake to create this one due to necessity to dig deep in drf to fix bugs there.
This field is a workaround to allow to write into SerializerMethodField.
"""
def __init__(self, method_name=None, **kwargs):
self.method_name = method_name
self.setter_method_name = kwargs.pop("setter_method_name", None)
self.deserializer_field = kwargs.pop("deserializer_field")
kwargs["source"] = "*"
super(serializers.SerializerMethodField, self).__init__(**kwargs)
def bind(self, field_name, parent):
retval = super().bind(field_name, parent)
if not self.setter_method_name:
self.setter_method_name = f"set_{field_name}"
return retval
def to_internal_value(self, data):
value = self.deserializer_field.to_internal_value(data)
method = getattr(self.parent, self.setter_method_name)
method(value)
return {self.method_name: value}
class CustomTimeField(fields.TimeField):
def to_representation(self, value):
result = super().to_representation(value)