2023-03-10 14:00:06 -03:00
|
|
|
from collections import defaultdict
|
|
|
|
|
|
|
|
|
|
from rest_framework import serializers
|
|
|
|
|
from rest_framework.validators import UniqueTogetherValidator
|
|
|
|
|
|
Webhook labels (#3383)
This PR add labels for webhooks.
1. Make webhook "labelable" with ability to filter by labels.
2. Add labels to the webhook payload. It contain new field webhook with
it's name, id and labels. Field integration and alert_group has a
corresponding label field as well. See example of a new payload below:
```
{
"event": {
"type": "escalation"
},
"user": null,
"alert_group": {
"id": "IRFN6ZD31N31B",
"integration_id": "CTWM7U4A2QG97",
"route_id": "RUE7U7Z46SKGY",
"alerts_count": 1,
"state": "firing",
"created_at": "2023-11-22T08:54:55.178243Z",
"resolved_at": null,
"acknowledged_at": null,
"title": "Incident",
"permalinks": {
"slack": null,
"telegram": null,
"web": "http://grafana:3000/a/grafana-oncall-app/alert-groups/IRFN6ZD31N31B"
},
"labels": {
"severity": "critical"
}
},
"alert_group_id": "IRFN6ZD31N31B",
"alert_payload": {
"message": "This alert was sent by user for demonstration purposes"
},
"integration": {
"id": "CTWM7U4A2QG97",
"type": "webhook",
"name": "hi - Webhook",
"team": null,
"labels": {
"hello": "world",
"severity": "critical"
}
},
"notified_users": [],
"users_to_be_notified": [],
"webhook": {
"id": "WHAXK4BTC7TAEQ",
"name": "test",
"labels": {
"hello": "kesha"
}
}
}
```
I feel that there is an opportunity to make code cleaner - remove all
label logic from serializers, views and utils to models or dedicated
LabelerService and introduce Labelable interface with something like
label_verbal, update_labels methods. However, I don't want to tie
webhook labels with a refactoring.
---------
Co-authored-by: Dominik <dominik.broj@grafana.com>
2023-11-22 19:17:41 +08:00
|
|
|
from apps.api.serializers.labels import LabelsSerializerMixin
|
2023-03-21 10:43:37 -03:00
|
|
|
from apps.webhooks.models import Webhook, WebhookResponse
|
2023-09-27 07:22:52 -06:00
|
|
|
from apps.webhooks.models.webhook import PUBLIC_WEBHOOK_HTTP_METHODS, WEBHOOK_FIELD_PLACEHOLDER
|
|
|
|
|
from apps.webhooks.presets.preset_options import WebhookPresetOptions
|
2024-02-23 08:55:44 -03:00
|
|
|
from common.api_helpers.custom_fields import IntegrationFilteredByOrganizationField, TeamPrimaryKeyRelatedField
|
2023-12-07 15:44:52 +01:00
|
|
|
from common.api_helpers.utils import CurrentOrganizationDefault, CurrentUserDefault
|
2023-03-10 14:00:06 -03:00
|
|
|
from common.jinja_templater import apply_jinja_template
|
|
|
|
|
from common.jinja_templater.apply_jinja_template import JinjaTemplateError, JinjaTemplateWarning
|
|
|
|
|
|
|
|
|
|
|
2023-03-21 10:43:37 -03:00
|
|
|
class WebhookResponseSerializer(serializers.ModelSerializer):
|
2023-03-10 14:00:06 -03:00
|
|
|
class Meta:
|
2023-03-21 10:43:37 -03:00
|
|
|
model = WebhookResponse
|
2023-03-10 14:00:06 -03:00
|
|
|
fields = [
|
2023-03-21 10:43:37 -03:00
|
|
|
"timestamp",
|
2023-03-10 14:00:06 -03:00
|
|
|
"url",
|
2023-03-21 10:43:37 -03:00
|
|
|
"request_trigger",
|
|
|
|
|
"request_headers",
|
|
|
|
|
"request_data",
|
|
|
|
|
"status_code",
|
|
|
|
|
"content",
|
2023-07-11 21:03:34 +03:00
|
|
|
"event_data",
|
2023-03-10 14:00:06 -03:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
Webhook labels (#3383)
This PR add labels for webhooks.
1. Make webhook "labelable" with ability to filter by labels.
2. Add labels to the webhook payload. It contain new field webhook with
it's name, id and labels. Field integration and alert_group has a
corresponding label field as well. See example of a new payload below:
```
{
"event": {
"type": "escalation"
},
"user": null,
"alert_group": {
"id": "IRFN6ZD31N31B",
"integration_id": "CTWM7U4A2QG97",
"route_id": "RUE7U7Z46SKGY",
"alerts_count": 1,
"state": "firing",
"created_at": "2023-11-22T08:54:55.178243Z",
"resolved_at": null,
"acknowledged_at": null,
"title": "Incident",
"permalinks": {
"slack": null,
"telegram": null,
"web": "http://grafana:3000/a/grafana-oncall-app/alert-groups/IRFN6ZD31N31B"
},
"labels": {
"severity": "critical"
}
},
"alert_group_id": "IRFN6ZD31N31B",
"alert_payload": {
"message": "This alert was sent by user for demonstration purposes"
},
"integration": {
"id": "CTWM7U4A2QG97",
"type": "webhook",
"name": "hi - Webhook",
"team": null,
"labels": {
"hello": "world",
"severity": "critical"
}
},
"notified_users": [],
"users_to_be_notified": [],
"webhook": {
"id": "WHAXK4BTC7TAEQ",
"name": "test",
"labels": {
"hello": "kesha"
}
}
}
```
I feel that there is an opportunity to make code cleaner - remove all
label logic from serializers, views and utils to models or dedicated
LabelerService and introduce Labelable interface with something like
label_verbal, update_labels methods. However, I don't want to tie
webhook labels with a refactoring.
---------
Co-authored-by: Dominik <dominik.broj@grafana.com>
2023-11-22 19:17:41 +08:00
|
|
|
class WebhookSerializer(LabelsSerializerMixin, serializers.ModelSerializer):
|
2023-03-10 14:00:06 -03:00
|
|
|
id = serializers.CharField(read_only=True, source="public_primary_key")
|
|
|
|
|
organization = serializers.HiddenField(default=CurrentOrganizationDefault())
|
2023-12-07 15:44:52 +01:00
|
|
|
team = TeamPrimaryKeyRelatedField(allow_null=True, required=False)
|
2023-03-10 14:00:06 -03:00
|
|
|
user = serializers.HiddenField(default=CurrentUserDefault())
|
|
|
|
|
forward_all = serializers.BooleanField(allow_null=True, required=False)
|
2023-03-21 10:43:37 -03:00
|
|
|
last_response_log = serializers.SerializerMethodField()
|
2023-09-27 07:22:52 -06:00
|
|
|
trigger_type = serializers.CharField(allow_null=True)
|
2023-03-10 14:00:06 -03:00
|
|
|
trigger_type_name = serializers.SerializerMethodField()
|
2024-02-23 08:55:44 -03:00
|
|
|
integration_filter = IntegrationFilteredByOrganizationField(
|
|
|
|
|
source="filtered_integrations", many=True, required=False
|
|
|
|
|
)
|
2023-03-10 14:00:06 -03:00
|
|
|
|
Webhook labels (#3383)
This PR add labels for webhooks.
1. Make webhook "labelable" with ability to filter by labels.
2. Add labels to the webhook payload. It contain new field webhook with
it's name, id and labels. Field integration and alert_group has a
corresponding label field as well. See example of a new payload below:
```
{
"event": {
"type": "escalation"
},
"user": null,
"alert_group": {
"id": "IRFN6ZD31N31B",
"integration_id": "CTWM7U4A2QG97",
"route_id": "RUE7U7Z46SKGY",
"alerts_count": 1,
"state": "firing",
"created_at": "2023-11-22T08:54:55.178243Z",
"resolved_at": null,
"acknowledged_at": null,
"title": "Incident",
"permalinks": {
"slack": null,
"telegram": null,
"web": "http://grafana:3000/a/grafana-oncall-app/alert-groups/IRFN6ZD31N31B"
},
"labels": {
"severity": "critical"
}
},
"alert_group_id": "IRFN6ZD31N31B",
"alert_payload": {
"message": "This alert was sent by user for demonstration purposes"
},
"integration": {
"id": "CTWM7U4A2QG97",
"type": "webhook",
"name": "hi - Webhook",
"team": null,
"labels": {
"hello": "world",
"severity": "critical"
}
},
"notified_users": [],
"users_to_be_notified": [],
"webhook": {
"id": "WHAXK4BTC7TAEQ",
"name": "test",
"labels": {
"hello": "kesha"
}
}
}
```
I feel that there is an opportunity to make code cleaner - remove all
label logic from serializers, views and utils to models or dedicated
LabelerService and introduce Labelable interface with something like
label_verbal, update_labels methods. However, I don't want to tie
webhook labels with a refactoring.
---------
Co-authored-by: Dominik <dominik.broj@grafana.com>
2023-11-22 19:17:41 +08:00
|
|
|
PREFETCH_RELATED = ["labels", "labels__key", "labels__value"]
|
|
|
|
|
|
2023-03-10 14:00:06 -03:00
|
|
|
class Meta:
|
|
|
|
|
model = Webhook
|
|
|
|
|
fields = [
|
|
|
|
|
"id",
|
|
|
|
|
"name",
|
2023-04-13 12:52:29 -06:00
|
|
|
"is_webhook_enabled",
|
Add is_legacy column to handle webhook migration (#1813)
Legacy webhooks won't be editable at first. Keep data templates
compatibility.
Possible migration code:
```python
from apps.webhooks.models import Webhook
from apps.alerts.models import CustomButton, EscalationPolicy
custom_buttons = CustomButton.objects.all()
for cb in custom_buttons:
webhook, _ = Webhook.objects.get_or_create(
organization=cb.organization,
team=cb.team,
name=cb.name,
is_legacy=True,
defaults=dict(
created_at=cb.created_at,
url=cb.webhook,
username=cb.user,
password=cb.password,
authorization_header=cb.authorization_header,
trigger_type=Webhook.TRIGGER_ESCALATION_STEP,
forward_all=cb.forward_whole_payload,
data=cb.data,
)
)
# migrate related escalation policies
policies = EscalationPolicy.objects.filter(
step=EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON,
custom_button_trigger=cb,
).update(
step=EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK,
custom_webhook=webhook,
)
```
2023-04-25 11:22:56 -03:00
|
|
|
"is_legacy",
|
2023-03-10 14:00:06 -03:00
|
|
|
"team",
|
|
|
|
|
"user",
|
|
|
|
|
"username",
|
|
|
|
|
"password",
|
|
|
|
|
"authorization_header",
|
|
|
|
|
"organization",
|
|
|
|
|
"trigger_template",
|
|
|
|
|
"headers",
|
|
|
|
|
"url",
|
|
|
|
|
"data",
|
|
|
|
|
"forward_all",
|
|
|
|
|
"http_method",
|
|
|
|
|
"trigger_type",
|
|
|
|
|
"trigger_type_name",
|
2023-03-21 10:43:37 -03:00
|
|
|
"last_response_log",
|
2023-04-13 12:52:29 -06:00
|
|
|
"integration_filter",
|
2023-09-27 07:22:52 -06:00
|
|
|
"preset",
|
Webhook labels (#3383)
This PR add labels for webhooks.
1. Make webhook "labelable" with ability to filter by labels.
2. Add labels to the webhook payload. It contain new field webhook with
it's name, id and labels. Field integration and alert_group has a
corresponding label field as well. See example of a new payload below:
```
{
"event": {
"type": "escalation"
},
"user": null,
"alert_group": {
"id": "IRFN6ZD31N31B",
"integration_id": "CTWM7U4A2QG97",
"route_id": "RUE7U7Z46SKGY",
"alerts_count": 1,
"state": "firing",
"created_at": "2023-11-22T08:54:55.178243Z",
"resolved_at": null,
"acknowledged_at": null,
"title": "Incident",
"permalinks": {
"slack": null,
"telegram": null,
"web": "http://grafana:3000/a/grafana-oncall-app/alert-groups/IRFN6ZD31N31B"
},
"labels": {
"severity": "critical"
}
},
"alert_group_id": "IRFN6ZD31N31B",
"alert_payload": {
"message": "This alert was sent by user for demonstration purposes"
},
"integration": {
"id": "CTWM7U4A2QG97",
"type": "webhook",
"name": "hi - Webhook",
"team": null,
"labels": {
"hello": "world",
"severity": "critical"
}
},
"notified_users": [],
"users_to_be_notified": [],
"webhook": {
"id": "WHAXK4BTC7TAEQ",
"name": "test",
"labels": {
"hello": "kesha"
}
}
}
```
I feel that there is an opportunity to make code cleaner - remove all
label logic from serializers, views and utils to models or dedicated
LabelerService and introduce Labelable interface with something like
label_verbal, update_labels methods. However, I don't want to tie
webhook labels with a refactoring.
---------
Co-authored-by: Dominik <dominik.broj@grafana.com>
2023-11-22 19:17:41 +08:00
|
|
|
"labels",
|
2023-03-10 14:00:06 -03:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
validators = [UniqueTogetherValidator(queryset=Webhook.objects.all(), fields=["name", "organization"])]
|
|
|
|
|
|
Webhook labels (#3383)
This PR add labels for webhooks.
1. Make webhook "labelable" with ability to filter by labels.
2. Add labels to the webhook payload. It contain new field webhook with
it's name, id and labels. Field integration and alert_group has a
corresponding label field as well. See example of a new payload below:
```
{
"event": {
"type": "escalation"
},
"user": null,
"alert_group": {
"id": "IRFN6ZD31N31B",
"integration_id": "CTWM7U4A2QG97",
"route_id": "RUE7U7Z46SKGY",
"alerts_count": 1,
"state": "firing",
"created_at": "2023-11-22T08:54:55.178243Z",
"resolved_at": null,
"acknowledged_at": null,
"title": "Incident",
"permalinks": {
"slack": null,
"telegram": null,
"web": "http://grafana:3000/a/grafana-oncall-app/alert-groups/IRFN6ZD31N31B"
},
"labels": {
"severity": "critical"
}
},
"alert_group_id": "IRFN6ZD31N31B",
"alert_payload": {
"message": "This alert was sent by user for demonstration purposes"
},
"integration": {
"id": "CTWM7U4A2QG97",
"type": "webhook",
"name": "hi - Webhook",
"team": null,
"labels": {
"hello": "world",
"severity": "critical"
}
},
"notified_users": [],
"users_to_be_notified": [],
"webhook": {
"id": "WHAXK4BTC7TAEQ",
"name": "test",
"labels": {
"hello": "kesha"
}
}
}
```
I feel that there is an opportunity to make code cleaner - remove all
label logic from serializers, views and utils to models or dedicated
LabelerService and introduce Labelable interface with something like
label_verbal, update_labels methods. However, I don't want to tie
webhook labels with a refactoring.
---------
Co-authored-by: Dominik <dominik.broj@grafana.com>
2023-11-22 19:17:41 +08:00
|
|
|
def create(self, validated_data):
|
|
|
|
|
organization = self.context["request"].auth.organization
|
|
|
|
|
labels = validated_data.pop("labels", None)
|
|
|
|
|
|
|
|
|
|
instance = super().create(validated_data)
|
|
|
|
|
self.update_labels_association_if_needed(labels, instance, organization)
|
|
|
|
|
return instance
|
|
|
|
|
|
|
|
|
|
def update(self, instance, validated_data):
|
|
|
|
|
labels = validated_data.pop("labels", None)
|
|
|
|
|
organization = self.context["request"].auth.organization
|
|
|
|
|
self.update_labels_association_if_needed(labels, instance, organization)
|
|
|
|
|
return super().update(instance, validated_data)
|
|
|
|
|
|
2023-06-06 01:59:12 -06:00
|
|
|
def to_representation(self, instance):
|
|
|
|
|
result = super().to_representation(instance)
|
|
|
|
|
if instance.password:
|
|
|
|
|
result["password"] = WEBHOOK_FIELD_PLACEHOLDER
|
|
|
|
|
if instance.authorization_header:
|
|
|
|
|
result["authorization_header"] = WEBHOOK_FIELD_PLACEHOLDER
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
def to_internal_value(self, data):
|
2023-07-20 15:23:33 -06:00
|
|
|
webhook = self.instance
|
|
|
|
|
|
2023-09-27 07:22:52 -06:00
|
|
|
# Some fields are conditionally required, add none values for missing required fields
|
|
|
|
|
if webhook and webhook.preset and "preset" not in data:
|
|
|
|
|
data["preset"] = webhook.preset
|
|
|
|
|
for key in ["url", "http_method", "trigger_type"]:
|
|
|
|
|
if key not in data:
|
|
|
|
|
if self.instance:
|
|
|
|
|
data[key] = getattr(self.instance, key)
|
|
|
|
|
else:
|
|
|
|
|
data[key] = None
|
|
|
|
|
|
2023-07-20 15:23:33 -06:00
|
|
|
# If webhook is being copied instance won't exist to copy values from
|
|
|
|
|
if not webhook and "id" in data:
|
|
|
|
|
webhook = Webhook.objects.get(
|
|
|
|
|
public_primary_key=data["id"], organization=self.context["request"].auth.organization
|
|
|
|
|
)
|
|
|
|
|
|
2023-06-06 01:59:12 -06:00
|
|
|
if data.get("password") == WEBHOOK_FIELD_PLACEHOLDER:
|
2023-07-20 15:23:33 -06:00
|
|
|
data["password"] = webhook.password
|
2023-06-06 01:59:12 -06:00
|
|
|
if data.get("authorization_header") == WEBHOOK_FIELD_PLACEHOLDER:
|
2023-07-20 15:23:33 -06:00
|
|
|
data["authorization_header"] = webhook.authorization_header
|
2024-03-05 14:11:47 -03:00
|
|
|
|
|
|
|
|
if not data.get("integration_filter"):
|
|
|
|
|
data["integration_filter"] = []
|
|
|
|
|
|
2023-06-06 01:59:12 -06:00
|
|
|
return super().to_internal_value(data)
|
|
|
|
|
|
2023-03-10 14:00:06 -03:00
|
|
|
def _validate_template_field(self, template):
|
|
|
|
|
try:
|
|
|
|
|
apply_jinja_template(template, alert_payload=defaultdict(str), alert_group_id="alert_group_1")
|
|
|
|
|
except JinjaTemplateError as e:
|
|
|
|
|
raise serializers.ValidationError(e.fallback_message)
|
|
|
|
|
except JinjaTemplateWarning:
|
|
|
|
|
# Suppress render exceptions since we do not have a representative payload to test with
|
|
|
|
|
pass
|
|
|
|
|
return template
|
|
|
|
|
|
|
|
|
|
def validate_trigger_template(self, trigger_template):
|
|
|
|
|
if not trigger_template:
|
|
|
|
|
return None
|
|
|
|
|
return self._validate_template_field(trigger_template)
|
|
|
|
|
|
|
|
|
|
def validate_headers(self, headers):
|
|
|
|
|
if not headers:
|
|
|
|
|
return None
|
|
|
|
|
return self._validate_template_field(headers)
|
|
|
|
|
|
|
|
|
|
def validate_url(self, url):
|
2023-09-27 07:22:52 -06:00
|
|
|
if self.is_field_controlled("url"):
|
|
|
|
|
return url
|
|
|
|
|
|
2023-03-10 14:00:06 -03:00
|
|
|
if not url:
|
2023-09-27 07:22:52 -06:00
|
|
|
raise serializers.ValidationError(detail="This field is required.")
|
2023-03-10 14:00:06 -03:00
|
|
|
return self._validate_template_field(url)
|
|
|
|
|
|
2023-09-27 07:22:52 -06:00
|
|
|
def validate_http_method(self, http_method):
|
|
|
|
|
if self.is_field_controlled("http_method"):
|
|
|
|
|
return http_method
|
|
|
|
|
|
|
|
|
|
if http_method not in PUBLIC_WEBHOOK_HTTP_METHODS:
|
|
|
|
|
raise serializers.ValidationError(detail=f"This field must be one of {PUBLIC_WEBHOOK_HTTP_METHODS}.")
|
|
|
|
|
return http_method
|
|
|
|
|
|
|
|
|
|
def validate_trigger_type(self, trigger_type):
|
|
|
|
|
if self.is_field_controlled("trigger_type"):
|
|
|
|
|
return trigger_type
|
|
|
|
|
|
|
|
|
|
if not trigger_type or int(trigger_type) not in Webhook.ALL_TRIGGER_TYPES:
|
|
|
|
|
raise serializers.ValidationError(detail="This field is required.")
|
|
|
|
|
return trigger_type
|
|
|
|
|
|
2023-03-10 14:00:06 -03:00
|
|
|
def validate_data(self, data):
|
|
|
|
|
if not data:
|
|
|
|
|
return None
|
|
|
|
|
return self._validate_template_field(data)
|
|
|
|
|
|
|
|
|
|
def validate_forward_all(self, data):
|
|
|
|
|
if data is None:
|
|
|
|
|
return False
|
|
|
|
|
return data
|
|
|
|
|
|
2023-09-27 07:22:52 -06:00
|
|
|
def validate_preset(self, preset):
|
|
|
|
|
if self.instance and self.instance.preset != preset:
|
|
|
|
|
raise serializers.ValidationError(detail="This field once set cannot be modified.")
|
|
|
|
|
|
|
|
|
|
if preset:
|
|
|
|
|
if preset not in WebhookPresetOptions.WEBHOOK_PRESETS:
|
|
|
|
|
raise serializers.ValidationError(detail=f"{preset} is not a valid preset id.")
|
|
|
|
|
|
|
|
|
|
preset_metadata = WebhookPresetOptions.WEBHOOK_PRESETS[preset].metadata
|
|
|
|
|
for controlled_field in preset_metadata.controlled_fields:
|
|
|
|
|
if controlled_field in self.initial_data:
|
|
|
|
|
if self.instance:
|
2024-03-05 14:11:47 -03:00
|
|
|
if bool(self.initial_data[controlled_field]) and self.initial_data[controlled_field] != getattr(
|
|
|
|
|
self.instance, controlled_field
|
|
|
|
|
):
|
2023-09-27 07:22:52 -06:00
|
|
|
raise serializers.ValidationError(
|
|
|
|
|
detail=f"{controlled_field} is controlled by preset, cannot update"
|
|
|
|
|
)
|
2024-03-05 14:11:47 -03:00
|
|
|
elif bool(self.initial_data[controlled_field]):
|
2023-09-27 07:22:52 -06:00
|
|
|
raise serializers.ValidationError(
|
|
|
|
|
detail=f"{controlled_field} is controlled by preset, cannot create"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return preset
|
|
|
|
|
|
2023-03-21 10:43:37 -03:00
|
|
|
def get_last_response_log(self, obj):
|
2023-11-22 23:08:04 -03:00
|
|
|
return WebhookResponseSerializer(obj.responses.last()).data
|
2023-03-10 14:00:06 -03:00
|
|
|
|
|
|
|
|
def get_trigger_type_name(self, obj):
|
|
|
|
|
trigger_type_name = ""
|
2023-04-05 09:03:55 -03:00
|
|
|
if obj.trigger_type is not None:
|
2023-03-10 14:00:06 -03:00
|
|
|
trigger_type_name = Webhook.TRIGGER_TYPES[int(obj.trigger_type)][1]
|
|
|
|
|
return trigger_type_name
|
2023-09-27 07:22:52 -06:00
|
|
|
|
|
|
|
|
def is_field_controlled(self, field_name):
|
|
|
|
|
if self.instance:
|
|
|
|
|
if not self.instance.preset:
|
|
|
|
|
return False
|
|
|
|
|
elif "preset" not in self.initial_data:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
preset_id = self.instance.preset if self.instance else self.initial_data["preset"]
|
|
|
|
|
if preset_id:
|
|
|
|
|
if preset_id not in WebhookPresetOptions.WEBHOOK_PRESETS:
|
|
|
|
|
raise serializers.ValidationError(detail=f"unknown preset {preset_id} referenced")
|
|
|
|
|
|
|
|
|
|
preset = WebhookPresetOptions.WEBHOOK_PRESETS[preset_id]
|
|
|
|
|
if field_name not in preset.metadata.controlled_fields:
|
|
|
|
|
return False
|
|
|
|
|
return True
|
2024-01-10 15:52:59 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class WebhookFastSerializer(serializers.ModelSerializer):
|
|
|
|
|
id = serializers.CharField(read_only=True, source="public_primary_key")
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
model = Webhook
|
|
|
|
|
fields = ["id", "name"]
|