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>
This commit is contained in:
2026-09-16 09:21:08 +02:00
co-authored by Claude Sonnet 5
parent f0d62bafd9
commit a705b64c22
+8 -27
View File
@@ -4,8 +4,10 @@ from dcim.api.serializers import (
DeviceRoleSerializer, DeviceTypeSerializer, LocationSerializer, PlatformSerializer, RackRoleSerializer, DeviceRoleSerializer, DeviceTypeSerializer, LocationSerializer, PlatformSerializer, RackRoleSerializer,
RackSerializer, RackTypeSerializer, SiteSerializer, RackSerializer, RackTypeSerializer, SiteSerializer,
) )
from dcim.models import Location, Rack, Site
from netbox.api.serializers import NetBoxModelSerializer from netbox.api.serializers import NetBoxModelSerializer
from tenancy.api.serializers import TenantGroupSerializer, TenantSerializer from tenancy.api.serializers import TenantGroupSerializer, TenantSerializer
from tenancy.models import Tenant
from ..choices import ConceptStatusChoices from ..choices import ConceptStatusChoices
from ..models import ConceptDeployment, ConceptDevice, RackConcept from ..models import ConceptDeployment, ConceptDevice, RackConcept
@@ -84,43 +86,31 @@ class ConceptDeploymentSerializer(serializers.ModelSerializer):
class RackCopySerializer(serializers.Serializer): class RackCopySerializer(serializers.Serializer):
"""POST body for copying a real rack into a new concept.""" """POST body for copying a real rack into a new concept."""
rack = serializers.PrimaryKeyRelatedField(queryset=None) rack = serializers.PrimaryKeyRelatedField(queryset=Rack.objects.all())
name = serializers.CharField(max_length=100) name = serializers.CharField(max_length=100)
status = serializers.ChoiceField(choices=ConceptStatusChoices, default=ConceptStatusChoices.STATUS_DRAFT) status = serializers.ChoiceField(choices=ConceptStatusChoices, default=ConceptStatusChoices.STATUS_DRAFT)
tenant = serializers.PrimaryKeyRelatedField(queryset=None, required=False, allow_null=True) tenant = serializers.PrimaryKeyRelatedField(queryset=Tenant.objects.all(), required=False, allow_null=True)
description = serializers.CharField(max_length=200, required=False, allow_blank=True, default='') description = serializers.CharField(max_length=200, required=False, allow_blank=True, default='')
include_devices = serializers.BooleanField(default=True) include_devices = serializers.BooleanField(default=True)
include_device_names = serializers.BooleanField(default=True) include_device_names = serializers.BooleanField(default=True)
def __init__(self, *args, **kwargs):
from dcim.models import Rack
from tenancy.models import Tenant
super().__init__(*args, **kwargs)
self.fields['rack'].queryset = Rack.objects.all()
self.fields['tenant'].queryset = Tenant.objects.all()
class RackConceptCloneSerializer(serializers.Serializer): class RackConceptCloneSerializer(serializers.Serializer):
"""POST body for duplicating a concept.""" """POST body for duplicating a concept."""
name = serializers.CharField(max_length=100) name = serializers.CharField(max_length=100)
status = serializers.ChoiceField(choices=ConceptStatusChoices, default=ConceptStatusChoices.STATUS_DRAFT) status = serializers.ChoiceField(choices=ConceptStatusChoices, default=ConceptStatusChoices.STATUS_DRAFT)
tenant = serializers.PrimaryKeyRelatedField(queryset=None, required=False, allow_null=True) tenant = serializers.PrimaryKeyRelatedField(queryset=Tenant.objects.all(), required=False, allow_null=True)
include_devices = serializers.BooleanField(default=True) include_devices = serializers.BooleanField(default=True)
def __init__(self, *args, **kwargs):
from tenancy.models import Tenant
super().__init__(*args, **kwargs)
self.fields['tenant'].queryset = Tenant.objects.all()
class ConceptDeploySerializer(serializers.Serializer): class ConceptDeploySerializer(serializers.Serializer):
"""POST body for materialising a concept.""" """POST body for materialising a concept."""
name_pattern = serializers.CharField(max_length=100) name_pattern = serializers.CharField(max_length=100)
count = serializers.IntegerField(min_value=1, max_value=100, default=1) count = serializers.IntegerField(min_value=1, max_value=100, default=1)
start_index = serializers.IntegerField(default=1) start_index = serializers.IntegerField(default=1)
site = serializers.PrimaryKeyRelatedField(queryset=None) site = serializers.PrimaryKeyRelatedField(queryset=Site.objects.all())
location = serializers.PrimaryKeyRelatedField(queryset=None, required=False, allow_null=True) location = serializers.PrimaryKeyRelatedField(queryset=Location.objects.all(), required=False, allow_null=True)
tenant = serializers.PrimaryKeyRelatedField(queryset=None, required=False, allow_null=True) tenant = serializers.PrimaryKeyRelatedField(queryset=Tenant.objects.all(), required=False, allow_null=True)
create_devices = serializers.BooleanField(default=True) create_devices = serializers.BooleanField(default=True)
device_name_pattern = serializers.CharField(max_length=100, required=False, allow_blank=True, default='') device_name_pattern = serializers.CharField(max_length=100, required=False, allow_blank=True, default='')
skip_unnamed_devices = serializers.BooleanField(default=True) skip_unnamed_devices = serializers.BooleanField(default=True)
@@ -129,16 +119,7 @@ class ConceptDeploySerializer(serializers.Serializer):
help_text='Return the planned rack names and warnings without creating anything' help_text='Return the planned rack names and warnings without creating anything'
) )
def __init__(self, *args, **kwargs):
from dcim.models import Location, Site
from tenancy.models import Tenant
super().__init__(*args, **kwargs)
self.fields['site'].queryset = Site.objects.all()
self.fields['location'].queryset = Location.objects.all()
self.fields['tenant'].queryset = Tenant.objects.all()
def validate(self, attrs): def validate(self, attrs):
from dcim.models import Rack
try: try:
names = [ names = [
attrs['name_pattern'].format(n=i) attrs['name_pattern'].format(n=i)