Fixes #20641: Handle viewsets with queryset=None in get_view_name() (#20642)
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run

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.
This commit is contained in:
Jason Novinger 2025-10-23 11:39:49 -05:00 committed by GitHub
parent ae5d7911f9
commit fb8d41b527
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 19 additions and 2 deletions

View File

@ -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()`. 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. 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. # Derive the model name from the queryset.
name = title(view.queryset.model._meta.verbose_name) name = title(view.queryset.model._meta.verbose_name)
if suffix := getattr(view, 'suffix', None): if suffix := getattr(view, 'suffix', None):

View File

@ -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 django.urls import reverse
from drf_spectacular.drainage import GENERATOR_STATS from drf_spectacular.drainage import GENERATOR_STATS
from rest_framework import status from rest_framework import status
@ -9,6 +9,7 @@ from extras.choices import CustomFieldTypeChoices
from extras.models import CustomField from extras.models import CustomField
from ipam.models import VLAN from ipam.models import VLAN
from netbox.config import get_config from netbox.config import get_config
from utilities.api import get_view_name
from utilities.testing import APITestCase, disable_warnings from utilities.testing import APITestCase, disable_warnings
@ -267,3 +268,19 @@ class APIDocsTestCase(TestCase):
with GENERATOR_STATS.silence(): # Suppress schema generator warnings with GENERATOR_STATS.silence(): # Suppress schema generator warnings
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200) 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')