Compare commits

..

1 Commits

Author SHA1 Message Date
Jeremy Stretch
bda369623c Fixes #21140: Ensure default panel attribute labels are translated 2026-01-14 08:56:50 -05:00
7 changed files with 12 additions and 69 deletions

View File

@@ -12,7 +12,7 @@ from django.core.validators import RegexValidator
from django.db import models
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext as _
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
from netbox.models import PrimaryModel
@@ -128,9 +128,7 @@ class DataSource(JobsMixin, PrimaryModel):
# Ensure URL scheme matches selected type
if self.backend_class.is_local and self.url_scheme not in ('file', ''):
raise ValidationError({
'source_url': _("URLs for local sources must start with {scheme} (or specify no scheme)").format(
scheme='file://'
)
'source_url': "URLs for local sources must start with file:// (or specify no scheme)"
})
def save(self, *args, **kwargs):

View File

@@ -38,15 +38,6 @@ class ScopedFilterMixin:
@dataclass
class ComponentModelFilterMixin:
_site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field(name='site')
)
_location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field(name='location')
)
_rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field(name='rack')
)
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
device_id: ID | None = strawberry_django.filter_field()
name: FilterLookup[str] | None = strawberry_django.filter_field()

View File

@@ -31,7 +31,7 @@ class RackDimensionsPanel(panels.ObjectAttributesPanel):
outer_width = attrs.NumericAttr('outer_width', unit_accessor='get_outer_unit_display')
outer_height = attrs.NumericAttr('outer_height', unit_accessor='get_outer_unit_display')
outer_depth = attrs.NumericAttr('outer_depth', unit_accessor='get_outer_unit_display')
mounting_depth = attrs.TextAttr('mounting_depth', format_string='{} mm')
mounting_depth = attrs.TextAttr('mounting_depth', format_string='{}mm')
class RackNumberingPanel(panels.ObjectAttributesPanel):

View File

@@ -37,6 +37,8 @@ class PluginMenuItem:
Alternatively, a pre-generated url can be set on the object which will be rendered literally.
Buttons are each specified as a list of PluginMenuButton instances.
"""
permissions = []
buttons = []
_url = None
def __init__(
@@ -52,14 +54,10 @@ class PluginMenuItem:
if type(permissions) not in (list, tuple):
raise TypeError(_("Permissions must be passed as a tuple or list."))
self.permissions = permissions
else:
self.permissions = []
if buttons is not None:
if type(buttons) not in (list, tuple):
raise TypeError(_("Buttons must be passed as a tuple or list."))
self.buttons = buttons
else:
self.buttons = []
@property
def url(self):
@@ -76,6 +74,7 @@ class PluginMenuButton:
ButtonColorChoices.
"""
color = ButtonColorChoices.DEFAULT
permissions = []
_url = None
def __init__(self, link, title, icon_class, color=None, permissions=None):
@@ -88,8 +87,6 @@ class PluginMenuButton:
if type(permissions) not in (list, tuple):
raise TypeError(_("Permissions must be passed as a tuple or list."))
self.permissions = permissions
else:
self.permissions = []
if color is not None:
if color not in ButtonColorChoices.values():
raise ValueError(_("Button color must be a choice within ButtonColorChoices."))

View File

@@ -11,7 +11,7 @@ from netbox.tests.dummy_plugin import config as dummy_config
from netbox.tests.dummy_plugin.data_backends import DummyBackend
from netbox.tests.dummy_plugin.jobs import DummySystemJob
from netbox.tests.dummy_plugin.webhook_callbacks import set_context
from netbox.plugins.navigation import PluginMenu, PluginMenuItem, PluginMenuButton
from netbox.plugins.navigation import PluginMenu
from netbox.plugins.utils import get_plugin_config
from netbox.graphql.schema import Query
from netbox.registry import registry
@@ -227,46 +227,3 @@ class PluginTest(TestCase):
Test the registration of webhook callbacks.
"""
self.assertIn(set_context, registry['webhook_callbacks'])
class PluginNavigationTest(TestCase):
def test_plugin_menu_item_independent_permissions(self):
item1 = PluginMenuItem(link='test1', link_text='Test 1')
item1.permissions.append('leaked_permission')
item2 = PluginMenuItem(link='test2', link_text='Test 2')
self.assertIsNot(item1.permissions, item2.permissions)
self.assertEqual(item1.permissions, ['leaked_permission'])
self.assertEqual(item2.permissions, [])
def test_plugin_menu_item_independent_buttons(self):
item1 = PluginMenuItem(link='test1', link_text='Test 1')
button = PluginMenuButton(link='button1', title='Button 1', icon_class='mdi-test')
item1.buttons.append(button)
item2 = PluginMenuItem(link='test2', link_text='Test 2')
self.assertIsNot(item1.buttons, item2.buttons)
self.assertEqual(len(item1.buttons), 1)
self.assertEqual(item1.buttons[0], button)
self.assertEqual(item2.buttons, [])
def test_plugin_menu_button_independent_permissions(self):
button1 = PluginMenuButton(link='button1', title='Button 1', icon_class='mdi-test')
button1.permissions.append('leaked_permission')
button2 = PluginMenuButton(link='button2', title='Button 2', icon_class='mdi-test')
self.assertIsNot(button1.permissions, button2.permissions)
self.assertEqual(button1.permissions, ['leaked_permission'])
self.assertEqual(button2.permissions, [])
def test_explicit_permissions_remain_independent(self):
item1 = PluginMenuItem(link='test1', link_text='Test 1', permissions=['explicit_permission'])
item2 = PluginMenuItem(link='test2', link_text='Test 2', permissions=['different_permission'])
self.assertIsNot(item1.permissions, item2.permissions)
self.assertEqual(item1.permissions, ['explicit_permission'])
self.assertEqual(item2.permissions, ['different_permission'])

View File

@@ -164,7 +164,7 @@ class ObjectAttributesPanel(ObjectPanel, metaclass=ObjectAttributesPanelMeta):
"""
label = name[:1].upper() + name[1:]
label = label.replace('_', ' ')
return label
return _(label)
def get_context(self, context):
# Determine which attributes to display in the panel based on only/exclude args

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-15 05:05+0000\n"
"POT-Creation-Date: 2026-01-13 05:05+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -12489,8 +12489,8 @@ msgstr ""
msgid "Delete Selected"
msgstr ""
#: netbox/netbox/plugins/navigation.py:53
#: netbox/netbox/plugins/navigation.py:89
#: netbox/netbox/plugins/navigation.py:55
#: netbox/netbox/plugins/navigation.py:88
msgid "Permissions must be passed as a tuple or list."
msgstr ""
@@ -12498,7 +12498,7 @@ msgstr ""
msgid "Buttons must be passed as a tuple or list."
msgstr ""
#: netbox/netbox/plugins/navigation.py:95
#: netbox/netbox/plugins/navigation.py:92
msgid "Button color must be a choice within ButtonColorChoices."
msgstr ""