Files
NetBox-Concept/netbox_rack_concept/api/serializers.py
T
MrBlakeandClaude Sonnet 5 a705b64c22 Fix AssertionError on API load: PrimaryKeyRelatedField needs a real queryset
DRF's RelatedField asserts queryset is not None (unless read_only) at
__init__ time, i.e. when the class body of the Serializer is
evaluated - not when the field is later used. The previous code passed
queryset=None and tried to patch it in the serializer's own __init__,
which never ran in time because construction of the field itself
already raised. Pass the real querysets directly at class-body level
instead, matching the standard DRF/NetBox pattern; the deferred
per-instance model imports are no longer needed since serializers are
only imported once all apps are ready anyway.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 09:21:08 +02:00

143 lines
6.7 KiB
Python

from rest_framework import serializers
from dcim.api.serializers import (
DeviceRoleSerializer, DeviceTypeSerializer, LocationSerializer, PlatformSerializer, RackRoleSerializer,
RackSerializer, RackTypeSerializer, SiteSerializer,
)
from dcim.models import Location, Rack, Site
from netbox.api.serializers import NetBoxModelSerializer
from tenancy.api.serializers import TenantGroupSerializer, TenantSerializer
from tenancy.models import Tenant
from ..choices import ConceptStatusChoices
from ..models import ConceptDeployment, ConceptDevice, RackConcept
__all__ = (
'ConceptDeploySerializer',
'ConceptDeploymentSerializer',
'ConceptDeviceSerializer',
'RackConceptCloneSerializer',
'RackConceptSerializer',
'RackCopySerializer',
)
class RackConceptSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:netbox_rack_concept-api:rackconcept-detail'
)
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
tenant_group = TenantGroupSerializer(nested=True, required=False, allow_null=True)
rack_type = RackTypeSerializer(nested=True, required=False, allow_null=True)
rack_role = RackRoleSerializer(nested=True, required=False, allow_null=True)
default_site = SiteSerializer(nested=True, required=False, allow_null=True)
default_location = LocationSerializer(nested=True, required=False, allow_null=True)
source_rack = RackSerializer(nested=True, required=False, allow_null=True)
device_count = serializers.IntegerField(read_only=True)
utilization = serializers.IntegerField(read_only=True)
class Meta:
model = RackConcept
fields = (
'id', 'url', 'display_url', 'display', 'name', 'status', 'description', 'tenant_group', 'tenant',
'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width',
'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'max_weight',
'default_site', 'default_location', 'rack_role', 'source_rack', 'comments', 'tags',
'custom_fields', 'created', 'last_updated', 'device_count', 'utilization',
)
brief_fields = ('id', 'url', 'display', 'name', 'status', 'u_height', 'description')
class ConceptDeviceSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:netbox_rack_concept-api:conceptdevice-detail'
)
concept = RackConceptSerializer(nested=True)
device_type = DeviceTypeSerializer(nested=True, required=False, allow_null=True)
role = DeviceRoleSerializer(nested=True, required=False, allow_null=True)
platform = PlatformSerializer(nested=True, required=False, allow_null=True)
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
effective_u_height = serializers.DecimalField(
max_digits=4, decimal_places=1, read_only=True
)
class Meta:
model = ConceptDevice
fields = (
'id', 'url', 'display_url', 'display', 'concept', 'name', 'label', 'status', 'device_type', 'role',
'platform', 'tenant', 'position', 'face', 'width_fraction', 'horizontal_position', 'u_height',
'is_full_depth', 'effective_u_height', 'serial', 'source_device', 'description', 'comments', 'tags',
'custom_fields', 'created', 'last_updated',
)
brief_fields = ('id', 'url', 'display', 'name', 'position', 'face', 'device_type')
class ConceptDeploymentSerializer(serializers.ModelSerializer):
rack = RackSerializer(nested=True, read_only=True, allow_null=True)
class Meta:
model = ConceptDeployment
fields = ('id', 'rack', 'rack_name', 'device_count', 'user', 'created')
#
# Action payloads
#
class RackCopySerializer(serializers.Serializer):
"""POST body for copying a real rack into a new concept."""
rack = serializers.PrimaryKeyRelatedField(queryset=Rack.objects.all())
name = serializers.CharField(max_length=100)
status = serializers.ChoiceField(choices=ConceptStatusChoices, default=ConceptStatusChoices.STATUS_DRAFT)
tenant = serializers.PrimaryKeyRelatedField(queryset=Tenant.objects.all(), required=False, allow_null=True)
description = serializers.CharField(max_length=200, required=False, allow_blank=True, default='')
include_devices = serializers.BooleanField(default=True)
include_device_names = serializers.BooleanField(default=True)
class RackConceptCloneSerializer(serializers.Serializer):
"""POST body for duplicating a concept."""
name = serializers.CharField(max_length=100)
status = serializers.ChoiceField(choices=ConceptStatusChoices, default=ConceptStatusChoices.STATUS_DRAFT)
tenant = serializers.PrimaryKeyRelatedField(queryset=Tenant.objects.all(), required=False, allow_null=True)
include_devices = serializers.BooleanField(default=True)
class ConceptDeploySerializer(serializers.Serializer):
"""POST body for materialising a concept."""
name_pattern = serializers.CharField(max_length=100)
count = serializers.IntegerField(min_value=1, max_value=100, default=1)
start_index = serializers.IntegerField(default=1)
site = serializers.PrimaryKeyRelatedField(queryset=Site.objects.all())
location = serializers.PrimaryKeyRelatedField(queryset=Location.objects.all(), required=False, allow_null=True)
tenant = serializers.PrimaryKeyRelatedField(queryset=Tenant.objects.all(), required=False, allow_null=True)
create_devices = serializers.BooleanField(default=True)
device_name_pattern = serializers.CharField(max_length=100, required=False, allow_blank=True, default='')
skip_unnamed_devices = serializers.BooleanField(default=True)
dry_run = serializers.BooleanField(
default=False,
help_text='Return the planned rack names and warnings without creating anything'
)
def validate(self, attrs):
try:
names = [
attrs['name_pattern'].format(n=i)
for i in range(attrs['start_index'], attrs['start_index'] + attrs['count'])
]
except (IndexError, KeyError, ValueError) as exc:
raise serializers.ValidationError({'name_pattern': f'Invalid pattern: {exc}'})
if len(set(names)) != len(names):
raise serializers.ValidationError({
'name_pattern': 'The pattern produces duplicate names. Include {n} when creating more than one rack.'
})
clashes = list(Rack.objects.filter(site=attrs['site'], name__in=names).values_list('name', flat=True))
if clashes:
raise serializers.ValidationError({
'name_pattern': f'These racks already exist at the site: {", ".join(sorted(clashes))}'
})
attrs['rack_names'] = names
return attrs