mirror of
https://github.com/netbox-community/netbox.git
synced 2026-02-17 22:37:46 +01:00
193 lines
6.6 KiB
Python
193 lines
6.6 KiB
Python
from django.contrib.contenttypes.models import ContentType
|
|
from django.core.exceptions import ObjectDoesNotExist
|
|
from django.db.backends.postgresql.psycopg_any import NumericRange
|
|
from django.utils.translation import gettext as _
|
|
from drf_spectacular.types import OpenApiTypes
|
|
from drf_spectacular.utils import extend_schema_field
|
|
from netaddr import IPNetwork
|
|
from rest_framework import serializers
|
|
from rest_framework.exceptions import ValidationError
|
|
from rest_framework.relations import PrimaryKeyRelatedField, RelatedField
|
|
|
|
__all__ = (
|
|
'AttributesField',
|
|
'ChoiceField',
|
|
'ContentTypeField',
|
|
'IPNetworkSerializer',
|
|
'IntegerRangeSerializer',
|
|
'RelatedObjectCountField',
|
|
'SerializedPKRelatedField',
|
|
)
|
|
|
|
|
|
class ChoiceField(serializers.Field):
|
|
"""
|
|
Represent a ChoiceField as {'value': <DB value>, 'label': <string>}. Accepts a single value on write.
|
|
|
|
:param choices: An iterable of choices in the form (value, key).
|
|
:param allow_blank: Allow blank values in addition to the listed choices.
|
|
"""
|
|
def __init__(self, choices, allow_blank=False, **kwargs):
|
|
self.choiceset = choices
|
|
self.allow_blank = allow_blank
|
|
self._choices = dict()
|
|
|
|
# Unpack grouped choices
|
|
for k, v in choices:
|
|
if type(v) in [list, tuple]:
|
|
for k2, v2 in v:
|
|
self._choices[k2] = v2
|
|
else:
|
|
self._choices[k] = v
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
def validate_empty_values(self, data):
|
|
# Convert null to an empty string unless allow_null == True
|
|
if data is None:
|
|
if self.allow_null:
|
|
return True, None
|
|
else:
|
|
data = ''
|
|
return super().validate_empty_values(data)
|
|
|
|
def to_representation(self, obj):
|
|
if obj != '':
|
|
# Use an empty string in place of the choice label if it cannot be resolved (i.e. because a previously
|
|
# configured choice has been removed from FIELD_CHOICES).
|
|
return {
|
|
'value': obj,
|
|
'label': self._choices.get(obj, ''),
|
|
}
|
|
|
|
def to_internal_value(self, data):
|
|
if data == '':
|
|
if self.allow_blank:
|
|
return data
|
|
raise ValidationError(_("This field may not be blank."))
|
|
|
|
# Provide an explicit error message if the request is trying to write a dict or list
|
|
if isinstance(data, (dict, list)):
|
|
raise ValidationError(
|
|
_('Value must be passed directly (e.g. "foo": 123); do not use a dictionary or list.')
|
|
)
|
|
|
|
# Check for string representations of boolean/integer values
|
|
if hasattr(data, 'lower'):
|
|
if data.lower() == 'true':
|
|
data = True
|
|
elif data.lower() == 'false':
|
|
data = False
|
|
else:
|
|
try:
|
|
data = int(data)
|
|
except ValueError:
|
|
pass
|
|
|
|
try:
|
|
if data in self._choices:
|
|
return data
|
|
except TypeError: # Input is an unhashable type
|
|
pass
|
|
|
|
raise ValidationError(_("{value} is not a valid choice.").format(value=data))
|
|
|
|
@property
|
|
def choices(self):
|
|
return self._choices
|
|
|
|
|
|
@extend_schema_field(OpenApiTypes.STR)
|
|
class ContentTypeField(RelatedField):
|
|
"""
|
|
Represent a ContentType as '<app_label>.<model>'
|
|
"""
|
|
default_error_messages = {
|
|
"does_not_exist": _("Invalid content type: {content_type}"),
|
|
"invalid": _("Invalid value. Specify a content type as '<app_label>.<model_name>'."),
|
|
}
|
|
|
|
def to_internal_value(self, data):
|
|
try:
|
|
app_label, model = data.split('.')
|
|
return ContentType.objects.get_by_natural_key(app_label=app_label, model=model)
|
|
except ObjectDoesNotExist:
|
|
self.fail('does_not_exist', content_type=data)
|
|
except (AttributeError, TypeError, ValueError):
|
|
self.fail('invalid')
|
|
|
|
def to_representation(self, obj):
|
|
return f"{obj.app_label}.{obj.model}"
|
|
|
|
|
|
class IPNetworkSerializer(serializers.Serializer):
|
|
"""
|
|
Representation of an IP network value (e.g. 192.0.2.0/24).
|
|
"""
|
|
def to_representation(self, instance):
|
|
return str(instance)
|
|
|
|
def to_internal_value(self, value):
|
|
return IPNetwork(value)
|
|
|
|
|
|
class SerializedPKRelatedField(PrimaryKeyRelatedField):
|
|
"""
|
|
Extends PrimaryKeyRelatedField to return a serialized object on read. This is useful for representing related
|
|
objects in a ManyToManyField while still allowing a set of primary keys to be written.
|
|
"""
|
|
def __init__(self, serializer, nested=False, **kwargs):
|
|
self.serializer = serializer
|
|
self.nested = nested
|
|
self.pk_field = kwargs.pop('pk_field', None)
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
def to_representation(self, value):
|
|
return self.serializer(value, nested=self.nested, context={'request': self.context['request']}).data
|
|
|
|
|
|
@extend_schema_field(OpenApiTypes.INT64)
|
|
class RelatedObjectCountField(serializers.ReadOnlyField):
|
|
"""
|
|
Represents a read-only integer count of related objects (e.g. the number of racks assigned to a site). This field
|
|
is detected by get_annotations_for_serializer() when determining the annotations to be added to a queryset
|
|
depending on the serializer fields selected for inclusion in the response.
|
|
"""
|
|
def __init__(self, relation, **kwargs):
|
|
self.relation = relation
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
|
|
class IntegerRangeSerializer(serializers.Serializer):
|
|
"""
|
|
Represents a range of integers.
|
|
"""
|
|
def to_internal_value(self, data):
|
|
if not isinstance(data, (list, tuple)) or len(data) != 2:
|
|
raise ValidationError(_("Ranges must be specified in the form (lower, upper)."))
|
|
if type(data[0]) is not int or type(data[1]) is not int:
|
|
raise ValidationError(_("Range boundaries must be defined as integers."))
|
|
|
|
return NumericRange(data[0], data[1] + 1, bounds='[)')
|
|
|
|
def to_representation(self, instance):
|
|
return instance.lower, instance.upper - 1
|
|
|
|
|
|
class AttributesField(serializers.JSONField):
|
|
"""
|
|
Custom attributes stored as JSON data.
|
|
"""
|
|
def to_internal_value(self, data):
|
|
data = super().to_internal_value(data)
|
|
|
|
# If updating an object, start with the initial attribute data. This enables the client to modify
|
|
# individual attributes without having to rewrite the entire field.
|
|
if data and self.parent.instance:
|
|
initial_data = getattr(self.parent.instance, self.source, None) or {}
|
|
return {**initial_data, **data}
|
|
|
|
return data
|