Serialize VID ranges as integer lists in REST API

This commit is contained in:
Jeremy Stretch 2024-07-13 15:00:16 -04:00
parent 45183e4066
commit 46a40c5745
4 changed files with 32 additions and 16 deletions

View File

@ -6,11 +6,10 @@ from dcim.api.serializers_.sites import SiteSerializer
from ipam.choices import * from ipam.choices import *
from ipam.constants import VLANGROUP_SCOPE_TYPES from ipam.constants import VLANGROUP_SCOPE_TYPES
from ipam.models import VLAN, VLANGroup from ipam.models import VLAN, VLANGroup
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField from netbox.api.fields import ChoiceField, ContentTypeField, IntegerRangeSerializer, RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer from netbox.api.serializers import NetBoxModelSerializer
from tenancy.api.serializers_.tenants import TenantSerializer from tenancy.api.serializers_.tenants import TenantSerializer
from utilities.api import get_serializer_for_model from utilities.api import get_serializer_for_model
from utilities.data import ranges_to_string, string_to_range_array
from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
from .roles import RoleSerializer from .roles import RoleSerializer
@ -22,14 +21,6 @@ __all__ = (
) )
class NumericRangeArraySerializer(serializers.CharField):
def to_internal_value(self, data):
return string_to_range_array(data)
def to_representation(self, instance):
return ranges_to_string(instance)
class VLANGroupSerializer(NetBoxModelSerializer): class VLANGroupSerializer(NetBoxModelSerializer):
scope_type = ContentTypeField( scope_type = ContentTypeField(
queryset=ContentType.objects.filter( queryset=ContentType.objects.filter(
@ -41,11 +32,11 @@ class VLANGroupSerializer(NetBoxModelSerializer):
) )
scope_id = serializers.IntegerField(allow_null=True, required=False, default=None) scope_id = serializers.IntegerField(allow_null=True, required=False, default=None)
scope = serializers.SerializerMethodField(read_only=True) scope = serializers.SerializerMethodField(read_only=True)
vlan_id_ranges = IntegerRangeSerializer(many=True, required=False)
utilization = serializers.CharField(read_only=True) utilization = serializers.CharField(read_only=True)
# Related object counts # Related object counts
vlan_count = RelatedObjectCountField('vlans') vlan_count = RelatedObjectCountField('vlans')
vlan_id_ranges = NumericRangeArraySerializer(required=False)
class Meta: class Meta:
model = VLANGroup model = VLANGroup

View File

@ -96,19 +96,24 @@ class VLANGroup(OrganizationalModel):
raise ValidationError(_("Cannot set scope_id without scope_type.")) raise ValidationError(_("Cannot set scope_id without scope_type."))
# Validate vlan ranges # Validate vlan ranges
if check_ranges_overlap(self.vlan_id_ranges): if self.vlan_id_ranges and check_ranges_overlap(self.vlan_id_ranges):
raise ValidationError({'vlan_id_ranges': _("Ranges cannot overlap.")}) raise ValidationError({'vlan_id_ranges': _("Ranges cannot overlap.")})
for ranges in self.vlan_id_ranges: for ranges in self.vlan_id_ranges:
if ranges.lower >= ranges.upper: if ranges.lower >= ranges.upper:
raise ValidationError({ raise ValidationError({
'vlan_id_ranges': _("Maximum child VID must be greater than or equal to minimum child VID Invalid range ({value})").format(value=ranges) 'vlan_id_ranges': _(
"Maximum child VID must be greater than or equal to minimum child VID Invalid range ({value})"
).format(value=ranges)
}) })
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.vlan_id_ranges:
self._total_vlan_ids = 0 self._total_vlan_ids = 0
for vlan_range in self.vlan_id_ranges: for vlan_range in self.vlan_id_ranges:
self._total_vlan_ids += vlan_range.upper - vlan_range.lower + 1 self._total_vlan_ids += vlan_range.upper - vlan_range.lower + 1
else:
self._total_vlan_ids = VLAN_VID_MAX - VLAN_VID_MIN + 1
super().save(*args, **kwargs) super().save(*args, **kwargs)

View File

@ -1,4 +1,5 @@
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db.backends.postgresql.psycopg_any import NumericRange
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field from drf_spectacular.utils import extend_schema_field
@ -11,6 +12,7 @@ __all__ = (
'ChoiceField', 'ChoiceField',
'ContentTypeField', 'ContentTypeField',
'IPNetworkSerializer', 'IPNetworkSerializer',
'IntegerRangeSerializer',
'RelatedObjectCountField', 'RelatedObjectCountField',
'SerializedPKRelatedField', 'SerializedPKRelatedField',
) )
@ -154,3 +156,19 @@ class RelatedObjectCountField(serializers.ReadOnlyField):
self.relation = relation self.relation = relation
super().__init__(**kwargs) 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])
def to_representation(self, instance):
return instance.lower, instance.upper

View File

@ -140,6 +140,8 @@ def ranges_to_string(ranges):
For example: For example:
[Range(1, 100), Range(200, 300)] => "1-100, 200-300" [Range(1, 100), Range(200, 300)] => "1-100, 200-300"
""" """
if not ranges:
return ''
return ', '.join([f"{r.lower}-{r.upper}" for r in ranges]) return ', '.join([f"{r.lower}-{r.upper}" for r in ranges])