From 0c70e9e140b660d83af85bc843cb9ae81b563f01 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Tue, 5 Aug 2025 07:26:43 -0500 Subject: [PATCH] Fixes #19986: Fix plugin list view button URLs (#20019) * Fixes #19986: Fix plugin list view button URLs Plugin list view action buttons (Add, Import, Export) were generating 404 errors because ObjectAction.get_url() was manually constructing viewnames without the required "plugins:" namespace prefix for plugin models. Replace manual viewname construction with NetBox's get_viewname() utility function, which properly handles plugin detection and namespace prefixing for both core and plugin models. * Ensure expected URL patterns are registered, ensures tests pass --- netbox/netbox/object_actions.py | 3 +- netbox/netbox/tests/dummy_plugin/urls.py | 3 ++ netbox/netbox/tests/test_object_actions.py | 32 ++++++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 netbox/netbox/tests/test_object_actions.py diff --git a/netbox/netbox/object_actions.py b/netbox/netbox/object_actions.py index 7a21739b9..51687eee1 100644 --- a/netbox/netbox/object_actions.py +++ b/netbox/netbox/object_actions.py @@ -6,6 +6,7 @@ from django.utils.translation import gettext as _ from core.models import ObjectType from extras.models import ExportTemplate from utilities.querydict import prepare_cloned_fields +from utilities.views import get_viewname __all__ = ( 'AddObject', @@ -42,7 +43,7 @@ class ObjectAction: @classmethod def get_url(cls, obj): - viewname = f'{obj._meta.app_label}:{obj._meta.model_name}_{cls.name}' + viewname = get_viewname(obj, action=cls.name) kwargs = { kwarg: getattr(obj, kwarg) for kwarg in cls.url_kwargs } diff --git a/netbox/netbox/tests/dummy_plugin/urls.py b/netbox/netbox/tests/dummy_plugin/urls.py index 6cdd48f7e..ff6b1cee6 100644 --- a/netbox/netbox/tests/dummy_plugin/urls.py +++ b/netbox/netbox/tests/dummy_plugin/urls.py @@ -7,5 +7,8 @@ urlpatterns = ( path('models/', views.DummyModelsView.as_view(), name='dummy_model_list'), path('models/add/', views.DummyModelAddView.as_view(), name='dummy_model_add'), + path('netboxmodel/', views.DummyNetBoxModelView.as_view(), name='dummynetboxmodel_list'), + path('netboxmodel/add/', views.DummyNetBoxModelView.as_view(), name='dummynetboxmodel_add'), + path('netboxmodel/import/', views.DummyNetBoxModelView.as_view(), name='dummynetboxmodel_bulk_import'), path('netboxmodel//', views.DummyNetBoxModelView.as_view(), name='dummynetboxmodel'), ) diff --git a/netbox/netbox/tests/test_object_actions.py b/netbox/netbox/tests/test_object_actions.py new file mode 100644 index 000000000..7e3b16bf1 --- /dev/null +++ b/netbox/netbox/tests/test_object_actions.py @@ -0,0 +1,32 @@ +from unittest import skipIf + +from django.conf import settings +from django.test import TestCase + +from dcim.models import Device +from netbox.object_actions import AddObject, BulkImport +from netbox.tests.dummy_plugin.models import DummyNetBoxModel + + +class ObjectActionTest(TestCase): + + def test_get_url_core_model(self): + """Test URL generation for core NetBox models""" + obj = Device() + + url = AddObject.get_url(obj) + self.assertEqual(url, '/dcim/devices/add/') + + url = BulkImport.get_url(obj) + self.assertEqual(url, '/dcim/devices/import/') + + @skipIf('netbox.tests.dummy_plugin' not in settings.PLUGINS, "dummy_plugin not in settings.PLUGINS") + def test_get_url_plugin_model(self): + """Test URL generation for plugin models includes plugins: namespace""" + obj = DummyNetBoxModel() + + url = AddObject.get_url(obj) + self.assertEqual(url, '/plugins/dummy-plugin/netboxmodel/add/') + + url = BulkImport.get_url(obj) + self.assertEqual(url, '/plugins/dummy-plugin/netboxmodel/import/')