From 977b10f71fb2c603370d2e5accb2c9f846e48c96 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Mon, 20 Oct 2025 21:23:01 -0500 Subject: [PATCH] Fixes #20641: Handle viewsets with queryset=None in get_view_name() The get_view_name() utility function crashed with AttributeError when called on viewsets that override get_queryset() without setting a class-level queryset attribute (e.g., ObjectChangeViewSet). This pattern became necessary in #20089 to force re-evaluation of valid_models() on each request, ensuring ObjectChange querysets reflect current ContentType state. Added None check to fall back to DRF's default view naming when no class-level queryset exists. --- netbox/utilities/api.py | 2 +- netbox/utilities/tests/test_api.py | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index 0aec1daef94..8d9e511ebb0 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -72,7 +72,7 @@ def get_view_name(view): Derive the view name from its associated model, if it has one. Fall back to DRF's built-in `get_view_name()`. This function is provided to DRF as its VIEW_NAME_FUNCTION. """ - if hasattr(view, 'queryset'): + if hasattr(view, 'queryset') and view.queryset is not None: # Derive the model name from the queryset. name = title(view.queryset.model._meta.verbose_name) if suffix := getattr(view, 'suffix', None): diff --git a/netbox/utilities/tests/test_api.py b/netbox/utilities/tests/test_api.py index f36827d8527..885f71a7398 100644 --- a/netbox/utilities/tests/test_api.py +++ b/netbox/utilities/tests/test_api.py @@ -1,4 +1,4 @@ -from django.test import Client, TestCase, override_settings +from django.test import Client, TestCase, override_settings, tag from django.urls import reverse from drf_spectacular.drainage import GENERATOR_STATS from rest_framework import status @@ -9,6 +9,7 @@ from extras.models import CustomField from ipam.models import VLAN from netbox.config import get_config +from utilities.api import get_view_name from utilities.testing import APITestCase, disable_warnings @@ -267,3 +268,19 @@ def test_api_docs(self): with GENERATOR_STATS.silence(): # Suppress schema generator warnings response = self.client.get(url) self.assertEqual(response.status_code, 200) + + +class GetViewNameTestCase(TestCase): + + @tag('regression') + def test_get_view_name_with_none_queryset(self): + from rest_framework.viewsets import ReadOnlyModelViewSet + + class MockViewSet(ReadOnlyModelViewSet): + queryset = None + + view = MockViewSet() + view.suffix = 'List' + + name = get_view_name(view) + self.assertEqual(name, 'Mock List')