From 44f173f01dc896bb83825f155016dd0282330dd9 Mon Sep 17 00:00:00 2001 From: bctiemann Date: Fri, 15 Aug 2025 14:34:21 -0400 Subject: [PATCH 01/22] Fixes: #20098 - Handle empty object_types field in Tag bulk import (#20099) --- netbox/utilities/forms/fields/csv.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netbox/utilities/forms/fields/csv.py b/netbox/utilities/forms/fields/csv.py index 0b33a1985..45428521c 100644 --- a/netbox/utilities/forms/fields/csv.py +++ b/netbox/utilities/forms/fields/csv.py @@ -114,6 +114,8 @@ class CSVMultipleContentTypeField(forms.ModelMultipleChoiceField): # TODO: Improve validation of selected ContentTypes def prepare_value(self, value): + if not value: + return None if type(value) is str: ct_filter = Q() for name in value.split(','): From 9f605a2db140bde5ee7a500d9489f50c53943f0b Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Fri, 15 Aug 2025 13:54:18 -0500 Subject: [PATCH 02/22] Fixes #19645: Correct Interface selection for Cable add when VC master is the selected device (#20041) * Fixes: #19645 - Correct Interface selection for Cable add when VC master is the selected device * Clarify label * Add test --- netbox/dcim/filtersets.py | 17 +++++++++++++++-- netbox/dcim/forms/connections.py | 7 ++++++- netbox/dcim/tests/test_filtersets.py | 16 ++++++++++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 814be356c..2a200ba3e 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1885,6 +1885,16 @@ class InterfaceFilterSet( PathEndpointFilterSet, CommonInterfaceFilterSet ): + virtual_chassis_member_or_master = MultiValueCharFilter( + method='filter_virtual_chassis_member_or_master', + field_name='name', + label=_('Virtual Chassis Interfaces for Device when device is master') + ) + virtual_chassis_member_or_master_id = MultiValueNumberFilter( + method='filter_virtual_chassis_member_or_master', + field_name='pk', + label=_('Virtual Chassis Interfaces for Device when device is master (ID)') + ) virtual_chassis_member = MultiValueCharFilter( method='filter_virtual_chassis_member', field_name='name', @@ -1995,11 +2005,14 @@ class InterfaceFilterSet( 'cable_id', 'cable_end', ) - def filter_virtual_chassis_member(self, queryset, name, value): + def filter_virtual_chassis_member_or_master(self, queryset, name, value): + return self.filter_virtual_chassis_member(queryset, name, value, if_master=True) + + def filter_virtual_chassis_member(self, queryset, name, value, if_master=False): try: vc_interface_ids = [] for device in Device.objects.filter(**{f'{name}__in': value}): - vc_interface_ids.extend(device.vc_interfaces(if_master=False).values_list('id', flat=True)) + vc_interface_ids.extend(device.vc_interfaces(if_master=if_master).values_list('id', flat=True)) return queryset.filter(pk__in=vc_interface_ids) except Device.DoesNotExist: return queryset.none() diff --git a/netbox/dcim/forms/connections.py b/netbox/dcim/forms/connections.py index 5e5d83b0b..d3588da39 100644 --- a/netbox/dcim/forms/connections.py +++ b/netbox/dcim/forms/connections.py @@ -19,6 +19,11 @@ def get_cable_form(a_type, b_type): # Device component if hasattr(term_cls, 'device'): + # Dynamically change the param field for interfaces to use virtual_chassis filter + query_param_device_field = 'device_id' + if term_cls == Interface: + query_param_device_field = 'virtual_chassis_member_or_master_id' + attrs[f'termination_{cable_end}_device'] = DynamicModelMultipleChoiceField( queryset=Device.objects.all(), label=_('Device'), @@ -36,7 +41,7 @@ def get_cable_form(a_type, b_type): 'parent': 'device', }, query_params={ - 'device_id': f'$termination_{cable_end}_device', + query_param_device_field: f'$termination_{cable_end}_device', 'kind': 'physical', # Exclude virtual interfaces } ) diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index 855c3abd3..0331de56c 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -4373,6 +4373,9 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil ) Device.objects.bulk_create(devices) + virtual_chassis.master = devices[0] + virtual_chassis.save() + module_bays = ( ModuleBay(device=devices[0], name='Module Bay 1'), ModuleBay(device=devices[1], name='Module Bay 2'), @@ -4759,6 +4762,19 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil params = {'device': [devices[0].name, devices[1].name]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_virtual_chassis_member_or_master(self): + vc = VirtualChassis.objects.first() + master = vc.master + member = vc.members.exclude(pk=master.pk).first() + params = {'virtual_chassis_member_or_master_id': [master.pk,]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'virtual_chassis_member_or_master_id': [member.pk,]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + params = {'virtual_chassis_member_or_master': [master.name,]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'virtual_chassis_member_or_master': [member.name,]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + def test_virtual_chassis_member(self): # Device 1A & 3 have 1 management interface, Device 1B has 1 interfaces devices = Device.objects.filter(name__in=['Device 1A', 'Device 3']) From a9ada4457bfdddc37f812c4447e08986b4f90646 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Fri, 15 Aug 2025 15:22:03 -0500 Subject: [PATCH 03/22] Fixes: #19669 & #18396 - Allow Token Authentication against Media view (#20046) --- netbox/netbox/views/misc.py | 4 ++-- netbox/utilities/views.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/netbox/netbox/views/misc.py b/netbox/netbox/views/misc.py index f28b0f7b1..ab995db66 100644 --- a/netbox/netbox/views/misc.py +++ b/netbox/netbox/views/misc.py @@ -20,7 +20,7 @@ from netbox.search.backends import search_backend from netbox.tables import SearchTable from utilities.htmx import htmx_partial from utilities.paginator import EnhancedPaginator, get_paginate_count -from utilities.views import ConditionalLoginRequiredMixin +from utilities.views import ConditionalLoginRequiredMixin, TokenConditionalLoginRequiredMixin __all__ = ( 'HomeView', @@ -119,7 +119,7 @@ class SearchView(ConditionalLoginRequiredMixin, View): }) -class MediaView(ConditionalLoginRequiredMixin, View): +class MediaView(TokenConditionalLoginRequiredMixin, View): """ Wrap Django's serve() view to enforce LOGIN_REQUIRED for static media. """ diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 1263874c4..5c6c7977d 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -7,6 +7,7 @@ from django.urls import reverse from django.urls.exceptions import NoReverseMatch from django.utils.translation import gettext_lazy as _ +from netbox.api.authentication import TokenAuthentication from netbox.plugins import PluginConfig from netbox.registry import registry from utilities.relations import get_related_models @@ -19,6 +20,7 @@ __all__ = ( 'GetRelatedModelsMixin', 'GetReturnURLMixin', 'ObjectPermissionRequiredMixin', + 'TokenConditionalLoginRequiredMixin', 'ViewTab', 'get_viewname', 'register_model_view', @@ -39,6 +41,19 @@ class ConditionalLoginRequiredMixin(AccessMixin): return super().dispatch(request, *args, **kwargs) +class TokenConditionalLoginRequiredMixin(ConditionalLoginRequiredMixin): + def dispatch(self, request, *args, **kwargs): + # Attempt to authenticate the user using a DRF token, if provided + if settings.LOGIN_REQUIRED and not request.user.is_authenticated: + authenticator = TokenAuthentication() + auth_info = authenticator.authenticate(request) + if auth_info is not None: + request.user = auth_info[0] # User object + request.auth = auth_info[1] + + return super().dispatch(request, *args, **kwargs) + + class ContentTypePermissionRequiredMixin(ConditionalLoginRequiredMixin): """ Similar to Django's built-in PermissionRequiredMixin, but extended to check model-level permission assignments. From 9580ac2946e4b702e249c37aaf9ec81a85229e19 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 16 Aug 2025 05:02:12 +0000 Subject: [PATCH 04/22] Update source translation strings --- netbox/translations/en/LC_MESSAGES/django.po | 114 ++++++++++--------- 1 file changed, 61 insertions(+), 53 deletions(-) diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index 2f68c04dd..2bd6ae9f3 100644 --- a/netbox/translations/en/LC_MESSAGES/django.po +++ b/netbox/translations/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-08-12 05:02+0000\n" +"POT-Creation-Date: 2025-08-16 05:01+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -165,8 +165,8 @@ msgstr "" #: netbox/dcim/filtersets.py:215 netbox/dcim/filtersets.py:336 #: netbox/dcim/filtersets.py:467 netbox/dcim/filtersets.py:1075 #: netbox/dcim/filtersets.py:1397 netbox/dcim/filtersets.py:1495 -#: netbox/dcim/filtersets.py:2175 netbox/dcim/filtersets.py:2418 -#: netbox/dcim/filtersets.py:2476 netbox/ipam/filtersets.py:954 +#: netbox/dcim/filtersets.py:2188 netbox/dcim/filtersets.py:2431 +#: netbox/dcim/filtersets.py:2489 netbox/ipam/filtersets.py:954 #: netbox/virtualization/filtersets.py:139 netbox/vpn/filtersets.py:361 msgid "Region (ID)" msgstr "" @@ -177,8 +177,8 @@ msgstr "" #: netbox/dcim/filtersets.py:222 netbox/dcim/filtersets.py:343 #: netbox/dcim/filtersets.py:474 netbox/dcim/filtersets.py:1082 #: netbox/dcim/filtersets.py:1404 netbox/dcim/filtersets.py:1502 -#: netbox/dcim/filtersets.py:2182 netbox/dcim/filtersets.py:2425 -#: netbox/dcim/filtersets.py:2483 netbox/extras/filtersets.py:602 +#: netbox/dcim/filtersets.py:2195 netbox/dcim/filtersets.py:2438 +#: netbox/dcim/filtersets.py:2496 netbox/extras/filtersets.py:602 #: netbox/ipam/filtersets.py:961 netbox/virtualization/filtersets.py:146 #: netbox/vpn/filtersets.py:356 msgid "Region (slug)" @@ -189,8 +189,8 @@ msgstr "" #: netbox/dcim/filtersets.py:131 netbox/dcim/filtersets.py:228 #: netbox/dcim/filtersets.py:349 netbox/dcim/filtersets.py:480 #: netbox/dcim/filtersets.py:1088 netbox/dcim/filtersets.py:1410 -#: netbox/dcim/filtersets.py:1508 netbox/dcim/filtersets.py:2188 -#: netbox/dcim/filtersets.py:2431 netbox/dcim/filtersets.py:2489 +#: netbox/dcim/filtersets.py:1508 netbox/dcim/filtersets.py:2201 +#: netbox/dcim/filtersets.py:2444 netbox/dcim/filtersets.py:2502 #: netbox/ipam/filtersets.py:239 netbox/ipam/filtersets.py:967 #: netbox/virtualization/filtersets.py:152 msgid "Site group (ID)" @@ -201,8 +201,8 @@ msgstr "" #: netbox/dcim/filtersets.py:138 netbox/dcim/filtersets.py:235 #: netbox/dcim/filtersets.py:356 netbox/dcim/filtersets.py:487 #: netbox/dcim/filtersets.py:1095 netbox/dcim/filtersets.py:1417 -#: netbox/dcim/filtersets.py:1515 netbox/dcim/filtersets.py:2195 -#: netbox/dcim/filtersets.py:2438 netbox/dcim/filtersets.py:2496 +#: netbox/dcim/filtersets.py:1515 netbox/dcim/filtersets.py:2208 +#: netbox/dcim/filtersets.py:2451 netbox/dcim/filtersets.py:2509 #: netbox/extras/filtersets.py:608 netbox/ipam/filtersets.py:246 #: netbox/ipam/filtersets.py:974 netbox/virtualization/filtersets.py:159 msgid "Site group (slug)" @@ -318,8 +318,8 @@ msgstr "" #: netbox/dcim/base_filtersets.py:47 netbox/dcim/filtersets.py:239 #: netbox/dcim/filtersets.py:360 netbox/dcim/filtersets.py:455 #: netbox/dcim/filtersets.py:1099 netbox/dcim/filtersets.py:1422 -#: netbox/dcim/filtersets.py:1520 netbox/dcim/filtersets.py:2200 -#: netbox/dcim/filtersets.py:2442 netbox/dcim/filtersets.py:2501 +#: netbox/dcim/filtersets.py:1520 netbox/dcim/filtersets.py:2213 +#: netbox/dcim/filtersets.py:2455 netbox/dcim/filtersets.py:2514 #: netbox/ipam/filtersets.py:251 netbox/ipam/filtersets.py:978 #: netbox/virtualization/filtersets.py:163 netbox/vpn/filtersets.py:371 msgid "Site (ID)" @@ -329,7 +329,7 @@ msgstr "" #: netbox/dcim/base_filtersets.py:59 netbox/dcim/filtersets.py:261 #: netbox/dcim/filtersets.py:372 netbox/dcim/filtersets.py:493 #: netbox/dcim/filtersets.py:1111 netbox/dcim/filtersets.py:1433 -#: netbox/dcim/filtersets.py:1531 netbox/dcim/filtersets.py:2454 +#: netbox/dcim/filtersets.py:1531 netbox/dcim/filtersets.py:2467 msgid "Location (ID)" msgstr "" @@ -341,7 +341,7 @@ msgstr "" #: netbox/circuits/filtersets.py:537 netbox/core/filtersets.py:81 #: netbox/core/filtersets.py:140 netbox/core/filtersets.py:177 #: netbox/dcim/filtersets.py:780 netbox/dcim/filtersets.py:1489 -#: netbox/dcim/filtersets.py:2549 netbox/extras/filtersets.py:45 +#: netbox/dcim/filtersets.py:2562 netbox/extras/filtersets.py:45 #: netbox/extras/filtersets.py:67 netbox/extras/filtersets.py:96 #: netbox/extras/filtersets.py:136 netbox/extras/filtersets.py:185 #: netbox/extras/filtersets.py:213 netbox/extras/filtersets.py:243 @@ -373,7 +373,7 @@ msgstr "" #: netbox/circuits/forms/model_forms.py:163 #: netbox/circuits/forms/model_forms.py:263 #: netbox/circuits/tables/circuits.py:107 -#: netbox/circuits/tables/circuits.py:203 netbox/dcim/forms/connections.py:73 +#: netbox/circuits/tables/circuits.py:203 netbox/dcim/forms/connections.py:78 #: netbox/templates/circuits/circuit.html:15 #: netbox/templates/circuits/circuitgroupassignment.html:30 #: netbox/templates/circuits/circuittermination.html:19 @@ -406,7 +406,7 @@ msgstr "" msgid "Virtual circuit (CID)" msgstr "" -#: netbox/circuits/filtersets.py:396 netbox/dcim/filtersets.py:1982 +#: netbox/circuits/filtersets.py:396 netbox/dcim/filtersets.py:1992 msgid "Virtual circuit (ID)" msgstr "" @@ -1895,7 +1895,7 @@ msgstr "" msgid "Assignments" msgstr "" -#: netbox/circuits/tables/circuits.py:116 netbox/dcim/forms/connections.py:81 +#: netbox/circuits/tables/circuits.py:116 netbox/dcim/forms/connections.py:86 msgid "Side" msgstr "" @@ -1949,7 +1949,7 @@ msgstr "" #: netbox/dcim/forms/bulk_import.py:1007 netbox/dcim/forms/bulk_import.py:1055 #: netbox/dcim/forms/bulk_import.py:1072 netbox/dcim/forms/bulk_import.py:1084 #: netbox/dcim/forms/bulk_import.py:1132 netbox/dcim/forms/bulk_import.py:1254 -#: netbox/dcim/forms/bulk_import.py:1650 netbox/dcim/forms/connections.py:24 +#: netbox/dcim/forms/bulk_import.py:1650 netbox/dcim/forms/connections.py:29 #: netbox/dcim/forms/filtersets.py:133 netbox/dcim/forms/filtersets.py:941 #: netbox/dcim/forms/filtersets.py:973 netbox/dcim/forms/filtersets.py:1119 #: netbox/dcim/forms/filtersets.py:1310 netbox/dcim/forms/filtersets.py:1335 @@ -3449,7 +3449,7 @@ msgstr "" #: netbox/dcim/filtersets.py:542 netbox/dcim/filtersets.py:707 #: netbox/dcim/filtersets.py:911 netbox/dcim/filtersets.py:985 #: netbox/dcim/filtersets.py:1025 netbox/dcim/filtersets.py:1368 -#: netbox/dcim/filtersets.py:2108 +#: netbox/dcim/filtersets.py:2121 msgid "Manufacturer (ID)" msgstr "" @@ -3457,7 +3457,7 @@ msgstr "" #: netbox/dcim/filtersets.py:548 netbox/dcim/filtersets.py:713 #: netbox/dcim/filtersets.py:917 netbox/dcim/filtersets.py:991 #: netbox/dcim/filtersets.py:1031 netbox/dcim/filtersets.py:1374 -#: netbox/dcim/filtersets.py:2114 +#: netbox/dcim/filtersets.py:2127 msgid "Manufacturer (slug)" msgstr "" @@ -3470,14 +3470,14 @@ msgid "Rack type (ID)" msgstr "" #: netbox/dcim/filtersets.py:414 netbox/dcim/filtersets.py:921 -#: netbox/dcim/filtersets.py:1047 netbox/dcim/filtersets.py:2118 +#: netbox/dcim/filtersets.py:1047 netbox/dcim/filtersets.py:2131 #: netbox/ipam/filtersets.py:376 netbox/ipam/filtersets.py:488 #: netbox/ipam/filtersets.py:998 netbox/virtualization/filtersets.py:177 msgid "Role (ID)" msgstr "" #: netbox/dcim/filtersets.py:420 netbox/dcim/filtersets.py:927 -#: netbox/dcim/filtersets.py:1054 netbox/dcim/filtersets.py:2124 +#: netbox/dcim/filtersets.py:1054 netbox/dcim/filtersets.py:2137 #: netbox/extras/filtersets.py:651 netbox/ipam/filtersets.py:382 #: netbox/ipam/filtersets.py:494 netbox/ipam/filtersets.py:1004 #: netbox/virtualization/filtersets.py:184 @@ -3486,7 +3486,7 @@ msgstr "" #: netbox/dcim/filtersets.py:450 netbox/dcim/filtersets.py:1123 #: netbox/dcim/filtersets.py:1444 netbox/dcim/filtersets.py:1542 -#: netbox/dcim/filtersets.py:2516 +#: netbox/dcim/filtersets.py:2529 msgid "Rack (ID)" msgstr "" @@ -3584,7 +3584,7 @@ msgstr "" msgid "Power port (ID)" msgstr "" -#: netbox/dcim/filtersets.py:907 netbox/dcim/filtersets.py:2104 +#: netbox/dcim/filtersets.py:907 netbox/dcim/filtersets.py:2117 msgid "Parent inventory item (ID)" msgstr "" @@ -3619,8 +3619,8 @@ msgid "Platform (slug)" msgstr "" #: netbox/dcim/filtersets.py:1105 netbox/dcim/filtersets.py:1428 -#: netbox/dcim/filtersets.py:1526 netbox/dcim/filtersets.py:2206 -#: netbox/dcim/filtersets.py:2448 netbox/dcim/filtersets.py:2507 +#: netbox/dcim/filtersets.py:1526 netbox/dcim/filtersets.py:2219 +#: netbox/dcim/filtersets.py:2461 netbox/dcim/filtersets.py:2520 msgid "Site name (slug)" msgstr "" @@ -3870,30 +3870,38 @@ msgid "VLAN Translation Policy" msgstr "" #: netbox/dcim/filtersets.py:1891 -msgid "Virtual Chassis Interfaces for Device" +msgid "Virtual Chassis Interfaces for Device when device is master" msgstr "" #: netbox/dcim/filtersets.py:1896 +msgid "Virtual Chassis Interfaces for Device when device is master (ID)" +msgstr "" + +#: netbox/dcim/filtersets.py:1901 +msgid "Virtual Chassis Interfaces for Device" +msgstr "" + +#: netbox/dcim/filtersets.py:1906 msgid "Virtual Chassis Interfaces for Device (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1900 +#: netbox/dcim/filtersets.py:1910 msgid "Kind of interface" msgstr "" -#: netbox/dcim/filtersets.py:1905 netbox/virtualization/filtersets.py:261 +#: netbox/dcim/filtersets.py:1915 netbox/virtualization/filtersets.py:261 msgid "Parent interface (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1910 netbox/virtualization/filtersets.py:266 +#: netbox/dcim/filtersets.py:1920 netbox/virtualization/filtersets.py:266 msgid "Bridged interface (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1915 +#: netbox/dcim/filtersets.py:1925 msgid "LAG interface (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1923 netbox/dcim/tables/devices.py:616 +#: netbox/dcim/filtersets.py:1933 netbox/dcim/tables/devices.py:616 #: netbox/dcim/tables/devices.py:1145 netbox/templates/dcim/interface.html:131 #: netbox/templates/dcim/macaddress.html:11 #: netbox/templates/dcim/macaddress.html:14 @@ -3901,77 +3909,77 @@ msgstr "" msgid "MAC Address" msgstr "" -#: netbox/dcim/filtersets.py:1928 netbox/virtualization/filtersets.py:275 +#: netbox/dcim/filtersets.py:1938 netbox/virtualization/filtersets.py:275 msgid "Primary MAC address (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1934 netbox/dcim/forms/model_forms.py:1540 +#: netbox/dcim/filtersets.py:1944 netbox/dcim/forms/model_forms.py:1540 #: netbox/virtualization/filtersets.py:281 #: netbox/virtualization/forms/model_forms.py:311 msgid "Primary MAC address" msgstr "" -#: netbox/dcim/filtersets.py:1956 netbox/dcim/filtersets.py:1968 +#: netbox/dcim/filtersets.py:1966 netbox/dcim/filtersets.py:1978 #: netbox/dcim/forms/filtersets.py:1423 netbox/dcim/forms/model_forms.py:1867 #: netbox/templates/dcim/virtualdevicecontext.html:15 msgid "Virtual Device Context" msgstr "" -#: netbox/dcim/filtersets.py:1962 +#: netbox/dcim/filtersets.py:1972 msgid "Virtual Device Context (Identifier)" msgstr "" -#: netbox/dcim/filtersets.py:1973 netbox/templates/wireless/wirelesslan.html:11 +#: netbox/dcim/filtersets.py:1983 netbox/templates/wireless/wirelesslan.html:11 #: netbox/wireless/forms/model_forms.py:57 msgid "Wireless LAN" msgstr "" -#: netbox/dcim/filtersets.py:1977 netbox/dcim/tables/devices.py:645 +#: netbox/dcim/filtersets.py:1987 netbox/dcim/tables/devices.py:645 msgid "Wireless link" msgstr "" -#: netbox/dcim/filtersets.py:1987 +#: netbox/dcim/filtersets.py:1997 msgid "Virtual circuit termination (ID)" msgstr "" -#: netbox/dcim/filtersets.py:2071 +#: netbox/dcim/filtersets.py:2084 msgid "Parent module bay (ID)" msgstr "" -#: netbox/dcim/filtersets.py:2076 +#: netbox/dcim/filtersets.py:2089 msgid "Installed module (ID)" msgstr "" -#: netbox/dcim/filtersets.py:2087 +#: netbox/dcim/filtersets.py:2100 msgid "Installed device (ID)" msgstr "" -#: netbox/dcim/filtersets.py:2093 +#: netbox/dcim/filtersets.py:2106 msgid "Installed device (name)" msgstr "" -#: netbox/dcim/filtersets.py:2163 +#: netbox/dcim/filtersets.py:2176 msgid "Master (ID)" msgstr "" -#: netbox/dcim/filtersets.py:2169 +#: netbox/dcim/filtersets.py:2182 msgid "Master (name)" msgstr "" -#: netbox/dcim/filtersets.py:2211 netbox/tenancy/filtersets.py:250 +#: netbox/dcim/filtersets.py:2224 netbox/tenancy/filtersets.py:250 msgid "Tenant (ID)" msgstr "" -#: netbox/dcim/filtersets.py:2217 netbox/extras/filtersets.py:711 +#: netbox/dcim/filtersets.py:2230 netbox/extras/filtersets.py:711 #: netbox/tenancy/filtersets.py:256 msgid "Tenant (slug)" msgstr "" -#: netbox/dcim/filtersets.py:2253 netbox/dcim/forms/filtersets.py:1145 +#: netbox/dcim/filtersets.py:2266 netbox/dcim/forms/filtersets.py:1145 msgid "Unterminated" msgstr "" -#: netbox/dcim/filtersets.py:2511 +#: netbox/dcim/filtersets.py:2524 msgid "Power panel (ID)" msgstr "" @@ -5213,7 +5221,7 @@ msgstr "" msgid "A {model} named {name} already exists" msgstr "" -#: netbox/dcim/forms/connections.py:49 netbox/dcim/forms/model_forms.py:843 +#: netbox/dcim/forms/connections.py:54 netbox/dcim/forms/model_forms.py:843 #: netbox/dcim/tables/power.py:66 #: netbox/templates/dcim/inc/cable_termination.html:42 #: netbox/templates/dcim/powerfeed.html:24 @@ -5222,7 +5230,7 @@ msgstr "" msgid "Power Panel" msgstr "" -#: netbox/dcim/forms/connections.py:58 netbox/dcim/forms/model_forms.py:871 +#: netbox/dcim/forms/connections.py:63 netbox/dcim/forms/model_forms.py:871 #: netbox/templates/dcim/powerfeed.html:21 #: netbox/templates/dcim/powerport.html:80 msgid "Power Feed" @@ -15997,17 +16005,17 @@ msgstr "" msgid "{value} is not a valid regular expression." msgstr "" -#: netbox/utilities/views.py:57 +#: netbox/utilities/views.py:72 #, python-brace-format msgid "{self.__class__.__name__} must implement get_required_permission()" msgstr "" -#: netbox/utilities/views.py:93 +#: netbox/utilities/views.py:108 #, python-brace-format msgid "{class_name} must implement get_required_permission()" msgstr "" -#: netbox/utilities/views.py:117 +#: netbox/utilities/views.py:132 #, python-brace-format msgid "" "{class_name} has no queryset defined. ObjectPermissionRequiredMixin may only " From db1786c3850390d48501db89b724554a2ffc6fc4 Mon Sep 17 00:00:00 2001 From: Tomas Neuner <31988980+9er-tom@users.noreply.github.com> Date: Wed, 20 Aug 2025 18:04:00 +0200 Subject: [PATCH 05/22] Fixes #19990: add optional return_url parameter to "Add" button for missing prerequisites (#20128) --- netbox/templates/inc/missing_prerequisites.html | 2 +- netbox/utilities/templates/buttons/add.html | 10 +++++----- netbox/utilities/templatetags/buttons.py | 3 ++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/netbox/templates/inc/missing_prerequisites.html b/netbox/templates/inc/missing_prerequisites.html index 7a28a7fe0..89d2f16ef 100644 --- a/netbox/templates/inc/missing_prerequisites.html +++ b/netbox/templates/inc/missing_prerequisites.html @@ -10,7 +10,7 @@ {% endblocktrans %}
- {% add_button prerequisite_model %} + {% add_button prerequisite_model request.path %}
diff --git a/netbox/utilities/templates/buttons/add.html b/netbox/utilities/templates/buttons/add.html index df257b5d2..278046852 100644 --- a/netbox/utilities/templates/buttons/add.html +++ b/netbox/utilities/templates/buttons/add.html @@ -1,6 +1,6 @@ {% if url %} -{% load i18n %} - - {% trans "Add" %} - -{% endif %} + {% load i18n %} + + {% trans "Add" %} + +{% endif %} \ No newline at end of file diff --git a/netbox/utilities/templatetags/buttons.py b/netbox/utilities/templatetags/buttons.py index d38c8863f..176ff4504 100644 --- a/netbox/utilities/templatetags/buttons.py +++ b/netbox/utilities/templatetags/buttons.py @@ -146,7 +146,7 @@ def sync_button(instance): # @register.inclusion_tag('buttons/add.html') -def add_button(model, action='add'): +def add_button(model, return_url=None, action='add'): try: url = reverse(get_viewname(model, action)) except NoReverseMatch: @@ -154,6 +154,7 @@ def add_button(model, action='add'): return { 'url': url, + 'return_url': return_url, } From 53c890c081c120094504d335bae13e689d89bea2 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Wed, 20 Aug 2025 11:09:21 -0500 Subject: [PATCH 06/22] Closes #20131: Add selector to the MACAddress `model_form` for `interface` and `vminterface` (#20132) --- netbox/dcim/forms/model_forms.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index d8cff372f..00b7733f1 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -1890,6 +1890,7 @@ class MACAddressForm(NetBoxModelForm): label=_('Interface'), queryset=Interface.objects.all(), required=False, + selector=True, context={ 'parent': 'device', }, @@ -1898,6 +1899,7 @@ class MACAddressForm(NetBoxModelForm): label=_('VM Interface'), queryset=VMInterface.objects.all(), required=False, + selector=True, context={ 'parent': 'virtual_machine', }, From 92fba0bed4106705ac0e2252664027a5051cdcdf Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 20 Aug 2025 16:13:33 -0400 Subject: [PATCH 07/22] Fixes #19970: Report device & VM counts for child device roles on parents --- netbox/dcim/api/serializers_/roles.py | 8 ++++---- netbox/dcim/api/views.py | 15 ++++++++++++++- netbox/dcim/views.py | 15 ++++++++++++--- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/netbox/dcim/api/serializers_/roles.py b/netbox/dcim/api/serializers_/roles.py index 17eeaa949..0f83655a6 100644 --- a/netbox/dcim/api/serializers_/roles.py +++ b/netbox/dcim/api/serializers_/roles.py @@ -1,3 +1,5 @@ +from rest_framework import serializers + from dcim.models import DeviceRole, InventoryItemRole from extras.api.serializers_.configtemplates import ConfigTemplateSerializer from netbox.api.fields import RelatedObjectCountField @@ -13,10 +15,8 @@ __all__ = ( class DeviceRoleSerializer(NestedGroupModelSerializer): parent = NestedDeviceRoleSerializer(required=False, allow_null=True, default=None) config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None) - - # Related object counts - device_count = RelatedObjectCountField('devices') - virtualmachine_count = RelatedObjectCountField('virtual_machines') + device_count = serializers.IntegerField(read_only=True, default=0) + virtualmachine_count = serializers.IntegerField(read_only=True, default=0) class Meta: model = DeviceRole diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index a64c9e5e3..eafe813a7 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -20,6 +20,7 @@ from netbox.api.viewsets import NetBoxModelViewSet, MPTTLockedMixin from netbox.api.viewsets.mixins import SequentialBulkCreatesMixin from utilities.api import get_serializer_for_model from utilities.query_functions import CollateAsChar +from virtualization.models import VirtualMachine from . import serializers from .exceptions import MissingFilterException @@ -351,7 +352,19 @@ class InventoryItemTemplateViewSet(MPTTLockedMixin, NetBoxModelViewSet): # class DeviceRoleViewSet(NetBoxModelViewSet): - queryset = DeviceRole.objects.all() + queryset = DeviceRole.objects.add_related_count( + DeviceRole.objects.add_related_count( + DeviceRole.objects.all(), + VirtualMachine, + 'role', + 'virtualmachine_count', + cumulative=True + ), + Device, + 'role', + 'device_count', + cumulative=True + ) serializer_class = serializers.DeviceRoleSerializer filterset_class = filtersets.DeviceRoleFilterSet diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 304438698..c53426f1a 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1990,9 +1990,18 @@ class InventoryItemTemplateBulkDeleteView(generic.BulkDeleteView): @register_model_view(DeviceRole, 'list', path='', detail=False) class DeviceRoleListView(generic.ObjectListView): - queryset = DeviceRole.objects.annotate( - device_count=count_related(Device, 'role'), - vm_count=count_related(VirtualMachine, 'role') + queryset = DeviceRole.objects.add_related_count( + DeviceRole.objects.add_related_count( + DeviceRole.objects.all(), + VirtualMachine, + 'role', + 'vm_count', + cumulative=True + ), + Device, + 'role', + 'device_count', + cumulative=True ) filterset = filtersets.DeviceRoleFilterSet filterset_form = forms.DeviceRoleFilterForm From f2b29273d09f193a8c5fd8053fd4c9ead1561792 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 21 Aug 2025 05:03:51 +0000 Subject: [PATCH 08/22] Update source translation strings --- netbox/translations/en/LC_MESSAGES/django.po | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index 2bd6ae9f3..0bcbc5dc8 100644 --- a/netbox/translations/en/LC_MESSAGES/django.po +++ b/netbox/translations/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-08-16 05:01+0000\n" +"POT-Creation-Date: 2025-08-21 05:03+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -1960,7 +1960,7 @@ msgstr "" #: netbox/dcim/forms/filtersets.py:1713 netbox/dcim/forms/filtersets.py:1737 #: netbox/dcim/forms/filtersets.py:1761 netbox/dcim/forms/model_forms.py:738 #: netbox/dcim/forms/model_forms.py:955 netbox/dcim/forms/model_forms.py:1356 -#: netbox/dcim/forms/model_forms.py:1841 netbox/dcim/forms/model_forms.py:1912 +#: netbox/dcim/forms/model_forms.py:1841 netbox/dcim/forms/model_forms.py:1914 #: netbox/dcim/forms/object_create.py:260 netbox/dcim/tables/connections.py:22 #: netbox/dcim/tables/connections.py:41 netbox/dcim/tables/connections.py:60 #: netbox/dcim/tables/devices.py:299 netbox/dcim/tables/devices.py:384 @@ -5622,11 +5622,11 @@ msgstr "" msgid "Inventory Item Role" msgstr "" -#: netbox/dcim/forms/model_forms.py:1898 +#: netbox/dcim/forms/model_forms.py:1899 msgid "VM Interface" msgstr "" -#: netbox/dcim/forms/model_forms.py:1913 netbox/ipam/forms/filtersets.py:631 +#: netbox/dcim/forms/model_forms.py:1915 netbox/ipam/forms/filtersets.py:631 #: netbox/ipam/forms/model_forms.py:334 netbox/ipam/tables/vlans.py:173 #: netbox/templates/virtualization/virtualdisk.html:21 #: netbox/templates/virtualization/virtualmachine.html:12 @@ -5643,7 +5643,7 @@ msgstr "" msgid "Virtual Machine" msgstr "" -#: netbox/dcim/forms/model_forms.py:1952 +#: netbox/dcim/forms/model_forms.py:1954 msgid "A MAC address can only be assigned to a single object." msgstr "" From 8b397f3b420c3f27de2867535ffa97a7694ceca8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 21 Aug 2025 10:27:01 -0400 Subject: [PATCH 09/22] Fixes #20012: Fix support for `empty` filter for custom fields (#20072) --- netbox/extras/lookups.py | 28 +++++++++++++++++++++++- netbox/extras/models/customfields.py | 10 ++++++++- netbox/extras/tests/test_customfields.py | 12 +++++++++- netbox/netbox/filtersets.py | 9 +++++++- 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/netbox/extras/lookups.py b/netbox/extras/lookups.py index 9e1fe4a0b..33296340e 100644 --- a/netbox/extras/lookups.py +++ b/netbox/extras/lookups.py @@ -1,4 +1,5 @@ -from django.db.models import CharField, Lookup +from django.db.models import CharField, JSONField, Lookup +from django.db.models.fields.json import KeyTextTransform from .fields import CachedValueField @@ -18,6 +19,30 @@ class Empty(Lookup): return f"CAST(LENGTH({sql}) AS BOOLEAN) IS TRUE", params +class JSONEmpty(Lookup): + """ + Support "empty" lookups for JSONField keys. + + A key is considered empty if it is "", null, or does not exist. + """ + lookup_name = "empty" + + def as_sql(self, compiler, connection): + # self.lhs.lhs is the parent expression (could be a JSONField or another KeyTransform) + # Rebuild the expression using KeyTextTransform to guarantee ->> (text) + text_expr = KeyTextTransform(self.lhs.key_name, self.lhs.lhs) + lhs_sql, lhs_params = compiler.compile(text_expr) + + value = self.rhs + if value not in (True, False): + raise ValueError("The 'empty' lookup only accepts True or False.") + + condition = '' if value else 'NOT ' + sql = f"(NULLIF({lhs_sql}, '') IS {condition}NULL)" + + return sql, lhs_params + + class NetHost(Lookup): """ Similar to ipam.lookups.NetHost, but casts the field to INET. @@ -45,5 +70,6 @@ class NetContainsOrEquals(Lookup): CharField.register_lookup(Empty) +JSONField.register_lookup(JSONEmpty) CachedValueField.register_lookup(NetHost) CachedValueField.register_lookup(NetContainsOrEquals) diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index aeeb15728..19d9e1ded 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -600,11 +600,19 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): kwargs = { 'field_name': f'custom_field_data__{self.name}' } + # Native numeric filters will use `isnull` by default for empty lookups, but + # JSON fields require `empty` (see bug #20012). + if lookup_expr == 'isnull': + lookup_expr = 'empty' if lookup_expr is not None: kwargs['lookup_expr'] = lookup_expr + # 'Empty' lookup is always a boolean + if lookup_expr == 'empty': + filter_class = django_filters.BooleanFilter + # Text/URL - if self.type in ( + elif self.type in ( CustomFieldTypeChoices.TYPE_TEXT, CustomFieldTypeChoices.TYPE_LONGTEXT, CustomFieldTypeChoices.TYPE_URL, diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index 8bae8dfc9..c3074aa41 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -1615,6 +1615,7 @@ class CustomFieldModelFilterTest(TestCase): 'cf11': manufacturers[2].pk, 'cf12': [manufacturers[2].pk, manufacturers[3].pk], }), + Site(name='Site 4', slug='site-4'), ]) def test_filter_integer(self): @@ -1624,6 +1625,7 @@ class CustomFieldModelFilterTest(TestCase): self.assertEqual(self.filterset({'cf_cf1__gte': [200]}, self.queryset).qs.count(), 2) self.assertEqual(self.filterset({'cf_cf1__lt': [200]}, self.queryset).qs.count(), 1) self.assertEqual(self.filterset({'cf_cf1__lte': [200]}, self.queryset).qs.count(), 2) + self.assertEqual(self.filterset({'cf_cf1__empty': True}, self.queryset).qs.count(), 1) def test_filter_decimal(self): self.assertEqual(self.filterset({'cf_cf2': [100.1, 200.2]}, self.queryset).qs.count(), 2) @@ -1632,6 +1634,7 @@ class CustomFieldModelFilterTest(TestCase): self.assertEqual(self.filterset({'cf_cf2__gte': [200.2]}, self.queryset).qs.count(), 2) self.assertEqual(self.filterset({'cf_cf2__lt': [200.2]}, self.queryset).qs.count(), 1) self.assertEqual(self.filterset({'cf_cf2__lte': [200.2]}, self.queryset).qs.count(), 2) + self.assertEqual(self.filterset({'cf_cf2__empty': True}, self.queryset).qs.count(), 1) def test_filter_boolean(self): self.assertEqual(self.filterset({'cf_cf3': True}, self.queryset).qs.count(), 2) @@ -1648,6 +1651,7 @@ class CustomFieldModelFilterTest(TestCase): self.assertEqual(self.filterset({'cf_cf4__niew': ['bar']}, self.queryset).qs.count(), 1) self.assertEqual(self.filterset({'cf_cf4__ie': ['FOO']}, self.queryset).qs.count(), 1) self.assertEqual(self.filterset({'cf_cf4__nie': ['FOO']}, self.queryset).qs.count(), 2) + self.assertEqual(self.filterset({'cf_cf4__empty': True}, self.queryset).qs.count(), 1) def test_filter_text_loose(self): self.assertEqual(self.filterset({'cf_cf5': ['foo']}, self.queryset).qs.count(), 2) @@ -1659,6 +1663,7 @@ class CustomFieldModelFilterTest(TestCase): self.assertEqual(self.filterset({'cf_cf6__gte': ['2016-06-27']}, self.queryset).qs.count(), 2) self.assertEqual(self.filterset({'cf_cf6__lt': ['2016-06-27']}, self.queryset).qs.count(), 1) self.assertEqual(self.filterset({'cf_cf6__lte': ['2016-06-27']}, self.queryset).qs.count(), 2) + self.assertEqual(self.filterset({'cf_cf6__empty': True}, self.queryset).qs.count(), 1) def test_filter_url_strict(self): self.assertEqual( @@ -1674,17 +1679,20 @@ class CustomFieldModelFilterTest(TestCase): self.assertEqual(self.filterset({'cf_cf7__niew': ['.com']}, self.queryset).qs.count(), 0) self.assertEqual(self.filterset({'cf_cf7__ie': ['HTTP://A.EXAMPLE.COM']}, self.queryset).qs.count(), 1) self.assertEqual(self.filterset({'cf_cf7__nie': ['HTTP://A.EXAMPLE.COM']}, self.queryset).qs.count(), 2) + self.assertEqual(self.filterset({'cf_cf7__empty': True}, self.queryset).qs.count(), 1) def test_filter_url_loose(self): self.assertEqual(self.filterset({'cf_cf8': ['example.com']}, self.queryset).qs.count(), 3) def test_filter_select(self): self.assertEqual(self.filterset({'cf_cf9': ['A', 'B']}, self.queryset).qs.count(), 2) + self.assertEqual(self.filterset({'cf_cf9__empty': True}, self.queryset).qs.count(), 1) def test_filter_multiselect(self): self.assertEqual(self.filterset({'cf_cf10': ['A']}, self.queryset).qs.count(), 1) self.assertEqual(self.filterset({'cf_cf10': ['A', 'C']}, self.queryset).qs.count(), 2) - self.assertEqual(self.filterset({'cf_cf10': ['null']}, self.queryset).qs.count(), 1) + self.assertEqual(self.filterset({'cf_cf10': ['null']}, self.queryset).qs.count(), 1) # Contains a literal null + self.assertEqual(self.filterset({'cf_cf10__empty': True}, self.queryset).qs.count(), 2) def test_filter_object(self): manufacturer_ids = Manufacturer.objects.values_list('id', flat=True) @@ -1692,6 +1700,7 @@ class CustomFieldModelFilterTest(TestCase): self.filterset({'cf_cf11': [manufacturer_ids[0], manufacturer_ids[1]]}, self.queryset).qs.count(), 2 ) + self.assertEqual(self.filterset({'cf_cf11__empty': True}, self.queryset).qs.count(), 1) def test_filter_multiobject(self): manufacturer_ids = Manufacturer.objects.values_list('id', flat=True) @@ -1703,3 +1712,4 @@ class CustomFieldModelFilterTest(TestCase): self.filterset({'cf_cf12': [manufacturer_ids[3]]}, self.queryset).qs.count(), 3 ) + self.assertEqual(self.filterset({'cf_cf12__empty': True}, self.queryset).qs.count(), 1) diff --git a/netbox/netbox/filtersets.py b/netbox/netbox/filtersets.py index f24b4e11c..ea24efe48 100644 --- a/netbox/netbox/filtersets.py +++ b/netbox/netbox/filtersets.py @@ -29,6 +29,13 @@ __all__ = ( 'OrganizationalModelFilterSet', ) +STANDARD_LOOKUPS = ( + 'exact', + 'iexact', + 'in', + 'contains', +) + # # FilterSets @@ -159,7 +166,7 @@ class BaseFilterSet(django_filters.FilterSet): return {} # Skip nonstandard lookup expressions - if existing_filter.method is not None or existing_filter.lookup_expr not in ['exact', 'iexact', 'in']: + if existing_filter.method is not None or existing_filter.lookup_expr not in STANDARD_LOOKUPS: return {} # Choose the lookup expression map based on the filter type From 2e809904fa6cb47ca8be1bda307a6bf362529db4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 21 Aug 2025 12:09:44 -0400 Subject: [PATCH 10/22] Fixes #20043: Prevent rack elevation SVG styling from overflowing to parent page (#20139) --- netbox/project-static/dist/rack_elevation.css | Bin 1514 -> 1512 bytes .../styles/svg/rack_elevation.scss | 6 ++---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/netbox/project-static/dist/rack_elevation.css b/netbox/project-static/dist/rack_elevation.css index 185163389fba2c7e71bcc7156f6377ebfa6eb32c..9a4d955726bd1bf342987c40b2ee1c2df711044d 100644 GIT binary patch delta 22 ecmaFG{epYLHO9#cn2H&#C*NQ^u~~%q8WR9=3<)X# delta 23 fcmaFC{fc|THAW__>d9VACnjHGEZ!{0e4PmZc2NmC diff --git a/netbox/project-static/styles/svg/rack_elevation.scss b/netbox/project-static/styles/svg/rack_elevation.scss index 603ad0a6c..7ec6aa8fa 100644 --- a/netbox/project-static/styles/svg/rack_elevation.scss +++ b/netbox/project-static/styles/svg/rack_elevation.scss @@ -28,10 +28,6 @@ svg { } } -* { - font-family: $font-family-sans-serif; - font-size: $font-size-sm; -} rect { box-sizing: border-box; } @@ -42,6 +38,8 @@ text { svg { background-color: var(--nbx-rack-bg); + font-family: $font-family-sans-serif; + font-size: $font-size-sm; // Rack unit numbers along left side of rack elevation. .unit { From 6b3b4b3193326dec2c759ce7517c99bf5ab38c40 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 22 Aug 2025 05:02:31 +0000 Subject: [PATCH 11/22] Update source translation strings --- netbox/translations/en/LC_MESSAGES/django.po | 50 ++++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index 0bcbc5dc8..d1ed555eb 100644 --- a/netbox/translations/en/LC_MESSAGES/django.po +++ b/netbox/translations/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-08-21 05:03+0000\n" +"POT-Creation-Date: 2025-08-22 05:02+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -350,7 +350,7 @@ msgstr "" #: netbox/extras/filtersets.py:496 netbox/extras/filtersets.py:556 #: netbox/extras/filtersets.py:591 netbox/extras/filtersets.py:750 #: netbox/extras/filtersets.py:800 netbox/ipam/forms/model_forms.py:492 -#: netbox/netbox/filtersets.py:289 netbox/netbox/forms/__init__.py:22 +#: netbox/netbox/filtersets.py:296 netbox/netbox/forms/__init__.py:22 #: netbox/netbox/forms/base.py:167 #: netbox/templates/htmx/object_selector.html:28 #: netbox/templates/inc/filter_list.html:42 @@ -8960,97 +8960,97 @@ msgstr "" msgid "Values must match this regex: {regex}" msgstr "" -#: netbox/extras/models/customfields.py:671 +#: netbox/extras/models/customfields.py:679 msgid "Value must be a string." msgstr "" -#: netbox/extras/models/customfields.py:673 +#: netbox/extras/models/customfields.py:681 #, python-brace-format msgid "Value must match regex '{regex}'" msgstr "" -#: netbox/extras/models/customfields.py:678 +#: netbox/extras/models/customfields.py:686 msgid "Value must be an integer." msgstr "" -#: netbox/extras/models/customfields.py:681 -#: netbox/extras/models/customfields.py:696 +#: netbox/extras/models/customfields.py:689 +#: netbox/extras/models/customfields.py:704 #, python-brace-format msgid "Value must be at least {minimum}" msgstr "" -#: netbox/extras/models/customfields.py:685 -#: netbox/extras/models/customfields.py:700 +#: netbox/extras/models/customfields.py:693 +#: netbox/extras/models/customfields.py:708 #, python-brace-format msgid "Value must not exceed {maximum}" msgstr "" -#: netbox/extras/models/customfields.py:693 +#: netbox/extras/models/customfields.py:701 msgid "Value must be a decimal." msgstr "" -#: netbox/extras/models/customfields.py:705 +#: netbox/extras/models/customfields.py:713 msgid "Value must be true or false." msgstr "" -#: netbox/extras/models/customfields.py:713 +#: netbox/extras/models/customfields.py:721 msgid "Date values must be in ISO 8601 format (YYYY-MM-DD)." msgstr "" -#: netbox/extras/models/customfields.py:722 +#: netbox/extras/models/customfields.py:730 msgid "Date and time values must be in ISO 8601 format (YYYY-MM-DD HH:MM:SS)." msgstr "" -#: netbox/extras/models/customfields.py:729 +#: netbox/extras/models/customfields.py:737 #, python-brace-format msgid "Invalid choice ({value}) for choice set {choiceset}." msgstr "" -#: netbox/extras/models/customfields.py:739 +#: netbox/extras/models/customfields.py:747 #, python-brace-format msgid "Invalid choice(s) ({value}) for choice set {choiceset}." msgstr "" -#: netbox/extras/models/customfields.py:748 +#: netbox/extras/models/customfields.py:756 #, python-brace-format msgid "Value must be an object ID, not {type}" msgstr "" -#: netbox/extras/models/customfields.py:754 +#: netbox/extras/models/customfields.py:762 #, python-brace-format msgid "Value must be a list of object IDs, not {type}" msgstr "" -#: netbox/extras/models/customfields.py:758 +#: netbox/extras/models/customfields.py:766 #, python-brace-format msgid "Found invalid object ID: {id}" msgstr "" -#: netbox/extras/models/customfields.py:761 +#: netbox/extras/models/customfields.py:769 msgid "Required field cannot be empty." msgstr "" -#: netbox/extras/models/customfields.py:781 +#: netbox/extras/models/customfields.py:789 msgid "Base set of predefined choices (optional)" msgstr "" -#: netbox/extras/models/customfields.py:793 +#: netbox/extras/models/customfields.py:801 msgid "Choices are automatically ordered alphabetically" msgstr "" -#: netbox/extras/models/customfields.py:800 +#: netbox/extras/models/customfields.py:808 msgid "custom field choice set" msgstr "" -#: netbox/extras/models/customfields.py:801 +#: netbox/extras/models/customfields.py:809 msgid "custom field choice sets" msgstr "" -#: netbox/extras/models/customfields.py:843 +#: netbox/extras/models/customfields.py:851 msgid "Must define base or extra choices." msgstr "" -#: netbox/extras/models/customfields.py:867 +#: netbox/extras/models/customfields.py:875 #, python-brace-format msgid "" "Cannot remove choice {choice} as there are {model} objects which reference " From d5e49c8cb0eef385426ac29eb436965d37058c27 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 22 Aug 2025 15:34:22 -0400 Subject: [PATCH 12/22] Closes #20122: Improve text contrast on highlighted changes (#20161) * Closes #20122: Improve text contrast on highlighted changes * Fix indentation --- netbox/project-static/dist/netbox.css | Bin 555435 -> 555527 bytes .../project-static/styles/custom/_code.scss | 12 ++++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/netbox/project-static/dist/netbox.css b/netbox/project-static/dist/netbox.css index ab11dafff1d8ac6bd5a31fd56b72d66ea3bb4679..c96b4220dd0ab9caa56f04096b97efca20b3dd8e 100644 GIT binary patch delta 134 zcmZ4eS+V_#VnYjK3sVbo3rh=Y3tJ2O7LNFW>07+mg_skIvZpiFF$%LQTNtGnTBJ=c mFW`_vmr6CW0D|dj3pm`-rJ%Za3ON+er69VtyA*Pya{vGV^Cuwy delta 80 zcmZqwqPY6AVnYjK3sVbo3rh=Y3tJ2O7LNFW=}yHQ64Uz%IOISqA+{7VV@qSp>9-3w U+#yndg&YdVQrj04a-?$r08LLBUjP6A diff --git a/netbox/project-static/styles/custom/_code.scss b/netbox/project-static/styles/custom/_code.scss index 0e6869efa..79bcb14c8 100644 --- a/netbox/project-static/styles/custom/_code.scss +++ b/netbox/project-static/styles/custom/_code.scss @@ -12,11 +12,13 @@ pre.change-data { min-width: fit-content; &.added { - background-color: $green; + color: var(--tblr-dark); + background-color: $green-300; } &.removed { - background-color: $red; + color: var(--tblr-dark); + background-color: $red-300; } } } @@ -26,11 +28,13 @@ pre.change-diff { border-color: transparent; &.change-added { - background-color: $green; + color: var(--tblr-dark); + background-color: $green-300; } &.change-removed { - background-color: $red; + color: var(--tblr-dark); + background-color: $red-300; } } From 66140fc017ba3dc8d8f75447da31a7572340a758 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 22 Aug 2025 19:01:34 -0400 Subject: [PATCH 13/22] Closes #18147: Include device & VM interfaces in VRF related objects (#20158) --- netbox/ipam/views.py | 20 +++++++++++++- .../templates/inc/panels/related_objects.html | 24 ++++++++--------- netbox/utilities/views.py | 26 +++++++++++++++---- 3 files changed, 52 insertions(+), 18 deletions(-) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 8efa06191..37e56ff3d 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -53,8 +53,26 @@ class VRFView(GetRelatedModelsMixin, generic.ObjectView): ) export_targets_table.configure(request) + related_models = self.get_related_models( + request, + instance, + omit=(Interface, VMInterface), + extra=( + ( + Interface.objects.restrict(request.user, 'view').filter(vrf=instance), + 'vrf_id', + _('Device Interfaces') + ), + ( + VMInterface.objects.restrict(request.user, 'view').filter(vrf=instance), + 'vrf_id', + _('VM Interfaces') + ), + ), + ) + return { - 'related_models': self.get_related_models(request, instance, omit=[Interface, VMInterface]), + 'related_models': related_models, 'import_targets_table': import_targets_table, 'export_targets_table': export_targets_table, } diff --git a/netbox/templates/inc/panels/related_objects.html b/netbox/templates/inc/panels/related_objects.html index c013c14c5..7e59e38c1 100644 --- a/netbox/templates/inc/panels/related_objects.html +++ b/netbox/templates/inc/panels/related_objects.html @@ -4,19 +4,19 @@

{% trans "Related Objects" %}