From 5b2241aaaf8f52c3ba425233e8d8cb815197fbb8 Mon Sep 17 00:00:00 2001 From: Antoine Keranflec'h Date: Thu, 10 Oct 2024 15:38:48 +0200 Subject: [PATCH 01/38] fix(17542) add contact to tunnels --- netbox/vpn/models/tunnels.py | 6 +++--- netbox/vpn/views.py | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/netbox/vpn/models/tunnels.py b/netbox/vpn/models/tunnels.py index 6f4fa4182..6224959eb 100644 --- a/netbox/vpn/models/tunnels.py +++ b/netbox/vpn/models/tunnels.py @@ -6,7 +6,7 @@ from django.urls import reverse from django.utils.translation import gettext_lazy as _ from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel -from netbox.models.features import CustomFieldsMixin, CustomLinksMixin, TagsMixin +from netbox.models.features import CustomFieldsMixin, CustomLinksMixin, TagsMixin, ContactsMixin from vpn.choices import * __all__ = ( @@ -16,7 +16,7 @@ __all__ = ( ) -class TunnelGroup(OrganizationalModel): +class TunnelGroup(ContactsMixin, OrganizationalModel): """ An administrative grouping of Tunnels. This can be used to correlate peer-to-peer tunnels which form a mesh, for example. @@ -30,7 +30,7 @@ class TunnelGroup(OrganizationalModel): return reverse('vpn:tunnelgroup', args=[self.pk]) -class Tunnel(PrimaryModel): +class Tunnel(ContactsMixin, PrimaryModel): name = models.CharField( verbose_name=_('name'), max_length=100, diff --git a/netbox/vpn/views.py b/netbox/vpn/views.py index ac8ce3667..73b2b04c0 100644 --- a/netbox/vpn/views.py +++ b/netbox/vpn/views.py @@ -62,6 +62,9 @@ class TunnelGroupBulkDeleteView(generic.BulkDeleteView): filterset = filtersets.TunnelGroupFilterSet table = tables.TunnelGroupTable +@register_model_view(Tunnel, 'contacts') +class TunnelGroupContactsView(ObjectContactsView): + queryset = TunnelGroup.objects.all() # # Tunnels @@ -121,6 +124,9 @@ class TunnelBulkDeleteView(generic.BulkDeleteView): filterset = filtersets.TunnelFilterSet table = tables.TunnelTable +@register_model_view(Tunnel, 'contacts') +class TunnelContactsView(ObjectContactsView): + queryset = Tunnel.objects.all() # # Tunnel terminations From 2a8728544c7fd1d3399f51d7de87c14b46f8a2d5 Mon Sep 17 00:00:00 2001 From: Antoine Keranflec'h Date: Fri, 31 Jan 2025 08:48:35 +0100 Subject: [PATCH 02/38] fix(pep) fix pep8 compliancy --- netbox/vpn/views.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/netbox/vpn/views.py b/netbox/vpn/views.py index 55d47ca62..1bcb99716 100644 --- a/netbox/vpn/views.py +++ b/netbox/vpn/views.py @@ -67,6 +67,7 @@ class TunnelGroupBulkDeleteView(generic.BulkDeleteView): filterset = filtersets.TunnelGroupFilterSet table = tables.TunnelGroupTable + @register_model_view(Tunnel, 'contacts') class TunnelGroupContactsView(ObjectContactsView): queryset = TunnelGroup.objects.all() @@ -75,6 +76,7 @@ class TunnelGroupContactsView(ObjectContactsView): # Tunnels # + @register_model_view(Tunnel, 'list', path='', detail=False) class TunnelListView(generic.ObjectListView): queryset = Tunnel.objects.annotate( @@ -134,6 +136,7 @@ class TunnelBulkDeleteView(generic.BulkDeleteView): filterset = filtersets.TunnelFilterSet table = tables.TunnelTable + @register_model_view(Tunnel, 'contacts') class TunnelContactsView(ObjectContactsView): queryset = Tunnel.objects.all() @@ -142,6 +145,7 @@ class TunnelContactsView(ObjectContactsView): # Tunnel terminations # + @register_model_view(TunnelTermination, 'list', path='', detail=False) class TunnelTerminationListView(generic.ObjectListView): queryset = TunnelTermination.objects.all() From 5a3e213fb4621b2208bac1def405806fdbaa9fab Mon Sep 17 00:00:00 2001 From: atownson <52260120+atownson@users.noreply.github.com> Date: Mon, 24 Feb 2025 09:07:16 -0600 Subject: [PATCH 03/38] Do not retrieve the data field from Job objects unless needed --- netbox/core/views.py | 6 +++--- netbox/netbox/views/generic/feature_views.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/netbox/core/views.py b/netbox/core/views.py index cd9cd6c67..194cbbc41 100644 --- a/netbox/core/views.py +++ b/netbox/core/views.py @@ -165,7 +165,7 @@ class DataFileBulkDeleteView(generic.BulkDeleteView): @register_model_view(Job, 'list', path='', detail=False) class JobListView(generic.ObjectListView): - queryset = Job.objects.all() + queryset = Job.objects.defer('data') filterset = filtersets.JobFilterSet filterset_form = forms.JobFilterForm table = tables.JobTable @@ -182,12 +182,12 @@ class JobView(generic.ObjectView): @register_model_view(Job, 'delete') class JobDeleteView(generic.ObjectDeleteView): - queryset = Job.objects.all() + queryset = Job.objects.defer('data') @register_model_view(Job, 'bulk_delete', path='delete', detail=False) class JobBulkDeleteView(generic.BulkDeleteView): - queryset = Job.objects.all() + queryset = Job.objects.defer('data') filterset = filtersets.JobFilterSet table = tables.JobTable diff --git a/netbox/netbox/views/generic/feature_views.py b/netbox/netbox/views/generic/feature_views.py index 1e17d5354..01c4b2862 100644 --- a/netbox/netbox/views/generic/feature_views.py +++ b/netbox/netbox/views/generic/feature_views.py @@ -166,7 +166,7 @@ class ObjectJobsView(ConditionalLoginRequiredMixin, View): def get_jobs(self, instance): object_type = ContentType.objects.get_for_model(instance) - return Job.objects.filter( + return Job.objects.defer('data').filter( object_type=object_type, object_id=instance.id ) From 64270d6a4e99770dab38870a866e14c72518b27b Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Mon, 24 Feb 2025 11:29:31 -0600 Subject: [PATCH 04/38] Fixes #18605: only VLANs at selected Site are shown in VLAN select --- netbox/ipam/forms/model_forms.py | 10 +++++++- netbox/ipam/tests/test_forms.py | 43 ++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 netbox/ipam/tests/test_forms.py diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index c381f99c9..e7fccfe82 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -212,7 +212,7 @@ class PrefixForm(TenancyForm, ScopedForm, NetBoxModelForm): required=False, selector=True, query_params={ - 'available_at_site': '$site', + 'available_at_site': '$scope', }, label=_('VLAN'), ) @@ -240,6 +240,14 @@ class PrefixForm(TenancyForm, ScopedForm, NetBoxModelForm): 'tenant', 'description', 'comments', 'tags', ] + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # #18605: only filter VLAN select list if scope field is a Site + if scope_field := self.fields.get('scope', None): + if scope_field.queryset.model is not Site: + self.fields['vlan'].widget.attrs.pop('data-dynamic-params', None) + class IPRangeForm(TenancyForm, NetBoxModelForm): vrf = DynamicModelChoiceField( diff --git a/netbox/ipam/tests/test_forms.py b/netbox/ipam/tests/test_forms.py new file mode 100644 index 000000000..d345c42b1 --- /dev/null +++ b/netbox/ipam/tests/test_forms.py @@ -0,0 +1,43 @@ +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from dcim.models import Location, Region, Site, SiteGroup +from ipam.forms import PrefixForm + + +class PrefixFormTestCase(TestCase): + default_dynamic_params = '[{"fieldName":"scope","queryParam":"available_at_site"}]' + + @classmethod + def setUpTestData(cls): + cls.site = Site.objects.create(name='Site 1', slug='site-1') + + def test_vlan_field_sets_dynamic_params_by_default(self): + """data-dynamic-params present when no scope_type selected""" + form = PrefixForm(data={}) + + assert form.fields['vlan'].widget.attrs['data-dynamic-params'] == self.default_dynamic_params + + def test_vlan_field_sets_dynamic_params_for_scope_site(self): + """data-dynamic-params present when scope type is Site and when scope is specifc site""" + form = PrefixForm(data={ + 'scope_type': ContentType.objects.get_for_model(Site).id, + 'scope': self.site, + }) + + assert form.fields['vlan'].widget.attrs['data-dynamic-params'] == self.default_dynamic_params + + def test_vlan_field_does_not_set_dynamic_params_for_other_scopes(self): + """data-dynamic-params not present when scope type is populated by is not Site""" + cases = [ + Region(name='Region 1', slug='region-1'), + Location(site=self.site, name='Location 1', slug='location-1'), + SiteGroup(name='Site Group 1', slug='site-group-1'), + ] + for case in cases: + form = PrefixForm(data={ + 'scope_type': ContentType.objects.get_for_model(case._meta.model).id, + 'scope': case, + }) + + assert 'data-dynamic-params' not in form.fields['vlan'].widget.attrs From beb0aff6569ca2f6656c765077cba68d69386453 Mon Sep 17 00:00:00 2001 From: Brian Tiemann Date: Sun, 23 Feb 2025 21:36:19 -0500 Subject: [PATCH 05/38] Add VLAN Translation Policy to bulk edit forms and tables for Interface and VMInterface --- netbox/dcim/forms/bulk_edit.py | 8 +++++--- netbox/dcim/tables/devices.py | 2 +- netbox/virtualization/forms/bulk_edit.py | 14 +++++++++++--- netbox/virtualization/tables/virtualmachines.py | 2 +- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index da5a45f15..e50804df8 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -1411,7 +1411,7 @@ class InterfaceBulkEditForm( form_from_model(Interface, [ 'label', 'type', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', - 'wireless_lans' + 'wireless_lans', 'vlan_translation_policy' ]) ): enabled = forms.NullBooleanField( @@ -1564,7 +1564,9 @@ class InterfaceBulkEditForm( FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')), FieldSet('poe_mode', 'poe_type', name=_('PoE')), FieldSet('parent', 'bridge', 'lag', name=_('Related Interfaces')), - FieldSet('mode', 'vlan_group', 'untagged_vlan', 'qinq_svlan', name=_('802.1Q Switching')), + FieldSet( + 'mode', 'vlan_group', 'untagged_vlan', 'qinq_svlan', 'vlan_translation_policy', name=_('802.1Q Switching') + ), FieldSet( TabbedGroups( FieldSet('tagged_vlans', name=_('Assignment')), @@ -1579,7 +1581,7 @@ class InterfaceBulkEditForm( nullable_fields = ( 'module', 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'wwn', 'vdcs', 'mtu', 'description', 'poe_mode', 'poe_type', 'mode', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', - 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vrf', 'wireless_lans' + 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vrf', 'wireless_lans', 'vlan_translation_policy', ) def __init__(self, *args, **kwargs): diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index d4f2f74b3..924ba0aaf 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -671,7 +671,7 @@ class InterfaceTable(BaseInterfaceTable, ModularDeviceComponentTable, PathEndpoi 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', - 'qinq_svlan', 'inventory_items', 'created', 'last_updated', + 'qinq_svlan', 'inventory_items', 'created', 'last_updated', 'vlan_translation_policy' ) default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description') diff --git a/netbox/virtualization/forms/bulk_edit.py b/netbox/virtualization/forms/bulk_edit.py index 3b5bf6c03..80b665047 100644 --- a/netbox/virtualization/forms/bulk_edit.py +++ b/netbox/virtualization/forms/bulk_edit.py @@ -6,7 +6,7 @@ from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN from dcim.forms.mixins import ScopedBulkEditForm from dcim.models import Device, DeviceRole, Platform, Site from extras.models import ConfigTemplate -from ipam.models import VLAN, VLANGroup, VRF +from ipam.models import VLAN, VLANGroup, VLANTranslationPolicy, VRF from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import BulkRenameForm, add_blank_choice @@ -242,15 +242,23 @@ class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm): required=False, label=_('VRF') ) + vlan_translation_policy = DynamicModelChoiceField( + queryset=VLANTranslationPolicy.objects.all(), + required=False, + label=_('VLAN Translation Policy') + ) model = VMInterface fieldsets = ( FieldSet('mtu', 'enabled', 'vrf', 'description'), FieldSet('parent', 'bridge', name=_('Related Interfaces')), - FieldSet('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', name=_('802.1Q Switching')), + FieldSet( + 'mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vlan_translation_policy', + name=_('802.1Q Switching') + ), ) nullable_fields = ( - 'parent', 'bridge', 'mtu', 'vrf', 'description', + 'parent', 'bridge', 'mtu', 'vrf', 'description', 'vlan_translation_policy', ) def __init__(self, *args, **kwargs): diff --git a/netbox/virtualization/tables/virtualmachines.py b/netbox/virtualization/tables/virtualmachines.py index 335d1de7d..20cfdd6d1 100644 --- a/netbox/virtualization/tables/virtualmachines.py +++ b/netbox/virtualization/tables/virtualmachines.py @@ -122,7 +122,7 @@ class VMInterfaceTable(BaseInterfaceTable): fields = ( 'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mtu', 'mode', 'description', 'tags', 'vrf', 'primary_mac_address', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', - 'qinq_svlan', 'created', 'last_updated', + 'qinq_svlan', 'created', 'last_updated', 'vlan_translation_policy', ) default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description') From 2a56c08bc8bd6a8038edf69e0b08e727fb71bc2c Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 05:02:09 +0000 Subject: [PATCH 06/38] Update source translation strings --- netbox/translations/en/LC_MESSAGES/django.po | 47 ++++++++++---------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index b51ee5ba6..650140c41 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-02-24 15:17+0000\n" +"POT-Creation-Date: 2025-02-25 05:01+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -473,8 +473,8 @@ msgstr "" #: netbox/dcim/forms/bulk_edit.py:1106 netbox/dcim/forms/bulk_edit.py:1150 #: netbox/dcim/forms/bulk_edit.py:1195 netbox/dcim/forms/bulk_edit.py:1222 #: netbox/dcim/forms/bulk_edit.py:1240 netbox/dcim/forms/bulk_edit.py:1258 -#: netbox/dcim/forms/bulk_edit.py:1276 netbox/dcim/forms/bulk_edit.py:1744 -#: netbox/dcim/forms/bulk_edit.py:1785 netbox/extras/forms/bulk_edit.py:39 +#: netbox/dcim/forms/bulk_edit.py:1276 netbox/dcim/forms/bulk_edit.py:1746 +#: netbox/dcim/forms/bulk_edit.py:1787 netbox/extras/forms/bulk_edit.py:39 #: netbox/extras/forms/bulk_edit.py:149 netbox/extras/forms/bulk_edit.py:178 #: netbox/extras/forms/bulk_edit.py:208 netbox/extras/forms/bulk_edit.py:256 #: netbox/extras/forms/bulk_edit.py:274 netbox/extras/forms/bulk_edit.py:298 @@ -582,7 +582,7 @@ msgstr "" #: netbox/virtualization/forms/bulk_edit.py:82 #: netbox/virtualization/forms/bulk_edit.py:159 #: netbox/virtualization/forms/bulk_edit.py:210 -#: netbox/virtualization/forms/bulk_edit.py:319 +#: netbox/virtualization/forms/bulk_edit.py:327 #: netbox/vpn/forms/bulk_edit.py:28 netbox/vpn/forms/bulk_edit.py:64 #: netbox/vpn/forms/bulk_edit.py:121 netbox/vpn/forms/bulk_edit.py:155 #: netbox/vpn/forms/bulk_edit.py:190 netbox/vpn/forms/bulk_edit.py:215 @@ -641,7 +641,7 @@ msgstr "" #: netbox/circuits/forms/filtersets.py:320 netbox/dcim/forms/bulk_edit.py:210 #: netbox/dcim/forms/bulk_edit.py:613 netbox/dcim/forms/bulk_edit.py:822 #: netbox/dcim/forms/bulk_edit.py:1191 netbox/dcim/forms/bulk_edit.py:1218 -#: netbox/dcim/forms/bulk_edit.py:1740 netbox/dcim/forms/filtersets.py:1065 +#: netbox/dcim/forms/bulk_edit.py:1742 netbox/dcim/forms/filtersets.py:1065 #: netbox/dcim/forms/filtersets.py:1323 netbox/dcim/forms/filtersets.py:1460 #: netbox/dcim/forms/filtersets.py:1484 netbox/dcim/tables/devices.py:737 #: netbox/dcim/tables/devices.py:793 netbox/dcim/tables/devices.py:1034 @@ -745,8 +745,8 @@ msgstr "" #: netbox/dcim/forms/bulk_edit.py:110 netbox/dcim/forms/bulk_edit.py:185 #: netbox/dcim/forms/bulk_edit.py:355 netbox/dcim/forms/bulk_edit.py:709 #: netbox/dcim/forms/bulk_edit.py:774 netbox/dcim/forms/bulk_edit.py:806 -#: netbox/dcim/forms/bulk_edit.py:933 netbox/dcim/forms/bulk_edit.py:1721 -#: netbox/dcim/forms/bulk_edit.py:1763 netbox/dcim/forms/bulk_import.py:90 +#: netbox/dcim/forms/bulk_edit.py:933 netbox/dcim/forms/bulk_edit.py:1723 +#: netbox/dcim/forms/bulk_edit.py:1765 netbox/dcim/forms/bulk_import.py:90 #: netbox/dcim/forms/bulk_import.py:149 netbox/dcim/forms/bulk_import.py:250 #: netbox/dcim/forms/bulk_import.py:532 netbox/dcim/forms/bulk_import.py:686 #: netbox/dcim/forms/bulk_import.py:1137 netbox/dcim/forms/bulk_import.py:1326 @@ -820,7 +820,7 @@ msgstr "" #: netbox/circuits/forms/filtersets.py:331 netbox/dcim/forms/bulk_edit.py:126 #: netbox/dcim/forms/bulk_edit.py:191 netbox/dcim/forms/bulk_edit.py:350 #: netbox/dcim/forms/bulk_edit.py:470 netbox/dcim/forms/bulk_edit.py:699 -#: netbox/dcim/forms/bulk_edit.py:812 netbox/dcim/forms/bulk_edit.py:1768 +#: netbox/dcim/forms/bulk_edit.py:812 netbox/dcim/forms/bulk_edit.py:1770 #: netbox/dcim/forms/bulk_import.py:109 netbox/dcim/forms/bulk_import.py:154 #: netbox/dcim/forms/bulk_import.py:243 netbox/dcim/forms/bulk_import.py:358 #: netbox/dcim/forms/bulk_import.py:506 netbox/dcim/forms/bulk_import.py:1338 @@ -1015,8 +1015,8 @@ msgstr "" #: netbox/circuits/forms/bulk_edit.py:233 netbox/dcim/forms/bulk_edit.py:969 #: netbox/dcim/forms/bulk_edit.py:1333 netbox/dcim/forms/bulk_edit.py:1350 #: netbox/dcim/forms/bulk_edit.py:1367 netbox/dcim/forms/bulk_edit.py:1385 -#: netbox/dcim/forms/bulk_edit.py:1480 netbox/dcim/forms/bulk_edit.py:1650 -#: netbox/dcim/forms/bulk_edit.py:1667 +#: netbox/dcim/forms/bulk_edit.py:1480 netbox/dcim/forms/bulk_edit.py:1652 +#: netbox/dcim/forms/bulk_edit.py:1669 msgid "Mark connected" msgstr "" @@ -1063,7 +1063,7 @@ msgstr "" #: netbox/circuits/forms/bulk_import.py:254 #: netbox/circuits/forms/filtersets.py:381 #: netbox/circuits/forms/model_forms.py:365 netbox/dcim/forms/bulk_edit.py:361 -#: netbox/dcim/forms/bulk_edit.py:1280 netbox/dcim/forms/bulk_edit.py:1711 +#: netbox/dcim/forms/bulk_edit.py:1280 netbox/dcim/forms/bulk_edit.py:1713 #: netbox/dcim/forms/bulk_import.py:255 netbox/dcim/forms/bulk_import.py:1106 #: netbox/dcim/forms/filtersets.py:368 netbox/dcim/forms/filtersets.py:778 #: netbox/dcim/forms/filtersets.py:1539 netbox/dcim/forms/model_forms.py:256 @@ -1329,7 +1329,7 @@ msgstr "" msgid "Term Side" msgstr "" -#: netbox/circuits/forms/filtersets.py:286 netbox/dcim/forms/bulk_edit.py:1570 +#: netbox/circuits/forms/filtersets.py:286 netbox/dcim/forms/bulk_edit.py:1572 #: netbox/extras/forms/model_forms.py:582 netbox/ipam/forms/filtersets.py:144 #: netbox/ipam/forms/filtersets.py:598 netbox/ipam/forms/model_forms.py:329 #: netbox/templates/dcim/macaddress.html:25 @@ -1905,7 +1905,7 @@ msgstr "" #: netbox/circuits/tables/virtual_circuits.py:109 #: netbox/dcim/forms/bulk_edit.py:745 netbox/dcim/forms/bulk_edit.py:1299 -#: netbox/dcim/forms/bulk_edit.py:1706 netbox/dcim/forms/bulk_edit.py:1758 +#: netbox/dcim/forms/bulk_edit.py:1708 netbox/dcim/forms/bulk_edit.py:1760 #: netbox/dcim/forms/bulk_import.py:668 netbox/dcim/forms/bulk_import.py:730 #: netbox/dcim/forms/bulk_import.py:756 netbox/dcim/forms/bulk_import.py:782 #: netbox/dcim/forms/bulk_import.py:802 netbox/dcim/forms/bulk_import.py:858 @@ -3142,7 +3142,7 @@ msgid "Virtual" msgstr "" #: netbox/dcim/choices.py:856 netbox/dcim/choices.py:1099 -#: netbox/dcim/forms/bulk_edit.py:1576 netbox/dcim/forms/filtersets.py:1335 +#: netbox/dcim/forms/bulk_edit.py:1578 netbox/dcim/forms/filtersets.py:1335 #: netbox/dcim/forms/model_forms.py:1007 netbox/dcim/forms/model_forms.py:1445 #: netbox/netbox/navigation/menu.py:146 netbox/netbox/navigation/menu.py:150 #: netbox/templates/dcim/interface.html:267 @@ -3744,6 +3744,7 @@ msgstr "" #: netbox/dcim/models/device_components.py:568 #: netbox/ipam/forms/filtersets.py:489 netbox/ipam/forms/model_forms.py:704 #: netbox/templates/ipam/vlantranslationpolicy.html:11 +#: netbox/virtualization/forms/bulk_edit.py:248 #: netbox/virtualization/forms/model_forms.py:373 msgid "VLAN Translation Policy" msgstr "" @@ -3903,7 +3904,7 @@ msgstr "" #: netbox/dcim/forms/bulk_edit.py:228 netbox/dcim/forms/bulk_edit.py:504 #: netbox/dcim/forms/bulk_edit.py:568 netbox/dcim/forms/bulk_edit.py:641 #: netbox/dcim/forms/bulk_edit.py:665 netbox/dcim/forms/bulk_edit.py:758 -#: netbox/dcim/forms/bulk_edit.py:1285 netbox/dcim/forms/bulk_edit.py:1716 +#: netbox/dcim/forms/bulk_edit.py:1285 netbox/dcim/forms/bulk_edit.py:1718 #: netbox/dcim/forms/bulk_import.py:184 netbox/dcim/forms/bulk_import.py:395 #: netbox/dcim/forms/bulk_import.py:429 netbox/dcim/forms/bulk_import.py:477 #: netbox/dcim/forms/bulk_import.py:513 netbox/dcim/forms/bulk_import.py:1112 @@ -4456,26 +4457,26 @@ msgstr "" #: netbox/dcim/forms/bulk_edit.py:1566 netbox/dcim/forms/model_forms.py:1437 #: netbox/templates/dcim/interface.html:105 -#: netbox/virtualization/forms/bulk_edit.py:249 +#: netbox/virtualization/forms/bulk_edit.py:254 #: netbox/virtualization/forms/model_forms.py:380 msgid "Related Interfaces" msgstr "" -#: netbox/dcim/forms/bulk_edit.py:1567 netbox/dcim/forms/model_forms.py:1441 -#: netbox/virtualization/forms/bulk_edit.py:250 +#: netbox/dcim/forms/bulk_edit.py:1568 netbox/dcim/forms/model_forms.py:1441 +#: netbox/virtualization/forms/bulk_edit.py:257 #: netbox/virtualization/forms/model_forms.py:383 msgid "802.1Q Switching" msgstr "" -#: netbox/dcim/forms/bulk_edit.py:1571 +#: netbox/dcim/forms/bulk_edit.py:1573 msgid "Add/Remove" msgstr "" -#: netbox/dcim/forms/bulk_edit.py:1630 netbox/dcim/forms/bulk_edit.py:1632 +#: netbox/dcim/forms/bulk_edit.py:1632 netbox/dcim/forms/bulk_edit.py:1634 msgid "Interface mode must be specified to assign VLANs" msgstr "" -#: netbox/dcim/forms/bulk_edit.py:1637 netbox/dcim/forms/common.py:51 +#: netbox/dcim/forms/bulk_edit.py:1639 netbox/dcim/forms/common.py:51 msgid "An access interface cannot have tagged VLANs assigned." msgstr "" @@ -4848,7 +4849,7 @@ msgstr "" #: netbox/virtualization/filtersets.py:254 #: netbox/virtualization/filtersets.py:305 #: netbox/virtualization/forms/bulk_edit.py:182 -#: netbox/virtualization/forms/bulk_edit.py:308 +#: netbox/virtualization/forms/bulk_edit.py:316 #: netbox/virtualization/forms/bulk_import.py:152 #: netbox/virtualization/forms/bulk_import.py:213 #: netbox/virtualization/forms/filtersets.py:217 @@ -15513,7 +15514,7 @@ msgstr "" msgid "Disk (MB)" msgstr "" -#: netbox/virtualization/forms/bulk_edit.py:316 +#: netbox/virtualization/forms/bulk_edit.py:324 #: netbox/virtualization/forms/filtersets.py:256 msgid "Size (MB)" msgstr "" From 59cd5bc653057d879e3b0dfd7553cdfcf85fa60d Mon Sep 17 00:00:00 2001 From: Antoine Keranflec'h Date: Tue, 25 Feb 2025 09:15:55 +0100 Subject: [PATCH 07/38] fix double line breaks --- netbox/vpn/models/tunnels.py | 2 +- netbox/vpn/views.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/netbox/vpn/models/tunnels.py b/netbox/vpn/models/tunnels.py index 0d4ef17aa..9372bd535 100644 --- a/netbox/vpn/models/tunnels.py +++ b/netbox/vpn/models/tunnels.py @@ -6,7 +6,7 @@ from django.urls import reverse from django.utils.translation import gettext_lazy as _ from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel -from netbox.models.features import CustomFieldsMixin, CustomLinksMixin, TagsMixin, ContactsMixin +from netbox.models.features import ContactsMixin, CustomFieldsMixin, CustomLinksMixin, TagsMixin from vpn.choices import * __all__ = ( diff --git a/netbox/vpn/views.py b/netbox/vpn/views.py index 1bcb99716..3253558a9 100644 --- a/netbox/vpn/views.py +++ b/netbox/vpn/views.py @@ -72,11 +72,11 @@ class TunnelGroupBulkDeleteView(generic.BulkDeleteView): class TunnelGroupContactsView(ObjectContactsView): queryset = TunnelGroup.objects.all() + # # Tunnels # - @register_model_view(Tunnel, 'list', path='', detail=False) class TunnelListView(generic.ObjectListView): queryset = Tunnel.objects.annotate( @@ -141,11 +141,11 @@ class TunnelBulkDeleteView(generic.BulkDeleteView): class TunnelContactsView(ObjectContactsView): queryset = Tunnel.objects.all() + # # Tunnel terminations # - @register_model_view(TunnelTermination, 'list', path='', detail=False) class TunnelTerminationListView(generic.ObjectListView): queryset = TunnelTermination.objects.all() From a6c07e6a35537da04e6eb9368d94d1901916afcf Mon Sep 17 00:00:00 2001 From: Antoine Keranflec'h Date: Tue, 25 Feb 2025 13:53:50 +0100 Subject: [PATCH 08/38] fix contact wrong model assignment --- netbox/vpn/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/vpn/views.py b/netbox/vpn/views.py index 3253558a9..bbed677c2 100644 --- a/netbox/vpn/views.py +++ b/netbox/vpn/views.py @@ -68,7 +68,7 @@ class TunnelGroupBulkDeleteView(generic.BulkDeleteView): table = tables.TunnelGroupTable -@register_model_view(Tunnel, 'contacts') +@register_model_view(TunnelGroup, 'contacts') class TunnelGroupContactsView(ObjectContactsView): queryset = TunnelGroup.objects.all() From 48b825c64a8dd1387de59f5063084ad0b857e41c Mon Sep 17 00:00:00 2001 From: atownson <52260120+atownson@users.noreply.github.com> Date: Tue, 25 Feb 2025 08:39:39 -0600 Subject: [PATCH 09/38] Closes #18024: Add URL pattern for scripts to reference them by module.name (#18723) * Add URL pattern for scripts to reference them by module.name * Change _get_script function name and syntax * Fix formatting issue --- netbox/extras/urls.py | 3 +++ netbox/extras/views.py | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py index 32633493f..f470f72b1 100644 --- a/netbox/extras/urls.py +++ b/netbox/extras/urls.py @@ -75,8 +75,11 @@ urlpatterns = [ path('scripts/add/', views.ScriptModuleCreateView.as_view(), name='scriptmodule_add'), path('scripts/results//', views.ScriptResultView.as_view(), name='script_result'), path('scripts//', views.ScriptView.as_view(), name='script'), + path('scripts/./', views.ScriptView.as_view(), name='script'), path('scripts//source/', views.ScriptSourceView.as_view(), name='script_source'), + path('scripts/./source/', views.ScriptSourceView.as_view(), name='script_source'), path('scripts//jobs/', views.ScriptJobsView.as_view(), name='script_jobs'), + path('scripts/./jobs/', views.ScriptJobsView.as_view(), name='script_jobs'), path('script-modules//', include(get_model_urls('extras', 'scriptmodule'))), # Markdown diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 86e7f214a..9b0eaebae 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -1251,6 +1251,14 @@ class ScriptListView(ContentTypePermissionRequiredMixin, View): class BaseScriptView(generic.ObjectView): queryset = Script.objects.all() + def get_object(self, **kwargs): + if pk := kwargs.get('pk', False): + return get_object_or_404(self.queryset, pk=pk) + elif (module := kwargs.get('module')) and (name := kwargs.get('name', False)): + return get_object_or_404(self.queryset, module__file_path=f'{module}.py', name=name) + else: + raise Http404 + def _get_script_class(self, script): """ Return an instance of the Script's Python class From bf836c9bc2f0582db8a737d3b03015529d731356 Mon Sep 17 00:00:00 2001 From: Alexander Haase Date: Tue, 25 Feb 2025 16:55:00 +0100 Subject: [PATCH 10/38] Fixes 17357: Use virtual chassis name as fallback for device (#18710) --- netbox/dcim/filtersets.py | 1 + netbox/dcim/models/devices.py | 26 +++++++++++++++----------- netbox/dcim/search.py | 1 + netbox/dcim/svg/racks.py | 6 ++---- netbox/dcim/tables/devices.py | 1 + netbox/dcim/tests/test_models.py | 26 ++++++++++++++++++++++++++ 6 files changed, 46 insertions(+), 15 deletions(-) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 60c3c4d38..fcb3c7e50 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1193,6 +1193,7 @@ class DeviceFilterSet( return queryset return queryset.filter( Q(name__icontains=value) | + Q(virtual_chassis__name__icontains=value) | Q(serial__icontains=value.strip()) | Q(inventoryitems__serial__icontains=value.strip()) | Q(asset_tag__icontains=value.strip()) | diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 12b0dae18..a4da28803 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -802,14 +802,10 @@ class Device( verbose_name_plural = _('devices') def __str__(self): - if self.name and self.asset_tag: - return f'{self.name} ({self.asset_tag})' - elif self.name: - return self.name - elif self.virtual_chassis and self.asset_tag: - return f'{self.virtual_chassis.name}:{self.vc_position} ({self.asset_tag})' - elif self.virtual_chassis: - return f'{self.virtual_chassis.name}:{self.vc_position} ({self.pk})' + if self.label and self.asset_tag: + return f'{self.label} ({self.asset_tag})' + elif self.label: + return self.label elif self.device_type and self.asset_tag: return f'{self.device_type.manufacturer} {self.device_type.model} ({self.asset_tag})' elif self.device_type: @@ -1073,14 +1069,22 @@ class Device( device.location = self.location device.save() + @property + def label(self): + """ + Return the device name if set; otherwise return a generated name if available. + """ + if self.name: + return self.name + if self.virtual_chassis: + return f'{self.virtual_chassis.name}:{self.vc_position}' + @property def identifier(self): """ Return the device name if set; otherwise return the Device's primary key as {pk} """ - if self.name is not None: - return self.name - return '{{{}}}'.format(self.pk) + return self.label or '{{{}}}'.format(self.pk) @property def primary_ip(self): diff --git a/netbox/dcim/search.py b/netbox/dcim/search.py index b964421de..964880990 100644 --- a/netbox/dcim/search.py +++ b/netbox/dcim/search.py @@ -44,6 +44,7 @@ class DeviceIndex(SearchIndex): ('asset_tag', 50), ('serial', 60), ('name', 100), + ('virtual_chassis', 200), ('description', 500), ('comments', 5000), ) diff --git a/netbox/dcim/svg/racks.py b/netbox/dcim/svg/racks.py index 94dbeeac2..de695664a 100644 --- a/netbox/dcim/svg/racks.py +++ b/netbox/dcim/svg/racks.py @@ -30,10 +30,8 @@ STROKE_RESERVED = '#4d4dff' def get_device_name(device): - if device.virtual_chassis: - name = f'{device.virtual_chassis.name}:{device.vc_position}' - elif device.name: - name = device.name + if device.label: + name = device.label else: name = str(device.device_type) if device.devicebay_count: diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 924ba0aaf..25875d7bb 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -143,6 +143,7 @@ class PlatformTable(NetBoxTable): class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): name = tables.TemplateColumn( verbose_name=_('Name'), + accessor=Accessor('label'), template_code=DEVICE_LINK, linkify=True ) diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index ff1eddd56..c8c84dafb 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -590,6 +590,32 @@ class DeviceTestCase(TestCase): device2.full_clean() device2.save() + def test_device_label(self): + device1 = Device( + site=Site.objects.first(), + device_type=DeviceType.objects.first(), + role=DeviceRole.objects.first(), + name=None, + ) + self.assertEqual(device1.label, None) + + device1.name = 'Test Device 1' + self.assertEqual(device1.label, 'Test Device 1') + + virtual_chassis = VirtualChassis.objects.create(name='VC 1') + device2 = Device( + site=Site.objects.first(), + device_type=DeviceType.objects.first(), + role=DeviceRole.objects.first(), + name=None, + virtual_chassis=virtual_chassis, + vc_position=2, + ) + self.assertEqual(device2.label, 'VC 1:2') + + device2.name = 'Test Device 2' + self.assertEqual(device2.label, 'Test Device 2') + def test_device_mismatched_site_cluster(self): cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1') Cluster.objects.create(name='Cluster 1', type=cluster_type) From 3b894f9ccb6e502573d4bcfa1c4f3f5a2f9dfdb4 Mon Sep 17 00:00:00 2001 From: Brian Tiemann Date: Tue, 25 Feb 2025 06:11:59 -0500 Subject: [PATCH 11/38] Handle null table in TableConfigForm --- netbox/utilities/forms/forms.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/netbox/utilities/forms/forms.py b/netbox/utilities/forms/forms.py index 0b92ce7b3..301be4886 100644 --- a/netbox/utilities/forms/forms.py +++ b/netbox/utilities/forms/forms.py @@ -132,8 +132,9 @@ class TableConfigForm(forms.Form): super().__init__(*args, **kwargs) # Initialize columns field based on table attributes - self.fields['available_columns'].choices = table.available_columns - self.fields['columns'].choices = table.selected_columns + if table: + self.fields['available_columns'].choices = table.available_columns + self.fields['columns'].choices = table.selected_columns @property def table_name(self): From cffb99cec52adcc001d2a27ad0b1dbecce2a3555 Mon Sep 17 00:00:00 2001 From: Renato Almeida de Oliveira Date: Tue, 25 Feb 2025 19:44:10 -0300 Subject: [PATCH 12/38] Fixes: #17796 Custom Field Choices -> Create & Add Another causes IndexError (#18631) --- netbox/extras/forms/model_forms.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index a45daaf70..be0b5856a 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -162,6 +162,7 @@ class CustomFieldForm(forms.ModelForm): class CustomFieldChoiceSetForm(forms.ModelForm): + # TODO: The extra_choices field definition diverge from the CustomFieldChoiceSet model extra_choices = forms.CharField( widget=ChoicesWidget(), required=False, @@ -178,12 +179,25 @@ class CustomFieldChoiceSetForm(forms.ModelForm): def __init__(self, *args, initial=None, **kwargs): super().__init__(*args, initial=initial, **kwargs) - # Escape colons in extra_choices + # TODO: The check for str / list below is to handle difference in extra_choices field definition + # In CustomFieldChoiceSetForm, extra_choices is a CharField but in CustomFieldChoiceSet, it is an ArrayField + # if standardize these, we can simplify this code + + # Convert extra_choices Array Field from model to CharField for form if 'extra_choices' in self.initial and self.initial['extra_choices']: - choices = [] - for choice in self.initial['extra_choices']: - choice = (choice[0].replace(':', '\\:'), choice[1].replace(':', '\\:')) - choices.append(choice) + extra_choices = self.initial['extra_choices'] + if isinstance(extra_choices, str): + extra_choices = [extra_choices] + choices = "" + for choice in extra_choices: + # Setup choices in Add Another use case + if isinstance(choice, str): + choice_str = ":".join(choice.replace("'", "").replace(" ", "")[1:-1].split(",")) + choices += choice_str + "\n" + # Setup choices in Edit use case + elif isinstance(choice, list): + choice_str = ":".join(choice) + choices += choice_str + "\n" self.initial['extra_choices'] = choices From c81f4da78002ccbee9a53a404d5fbc042861b846 Mon Sep 17 00:00:00 2001 From: Marcus Weiner Date: Wed, 26 Feb 2025 01:34:15 +0100 Subject: [PATCH 13/38] Fix definition of `vid_ranges` in `VLANGroup` so it shows up in the OpenAPI schema (#18237) --- netbox/core/api/schema.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/netbox/core/api/schema.py b/netbox/core/api/schema.py index 663ee2899..84bfae3bc 100644 --- a/netbox/core/api/schema.py +++ b/netbox/core/api/schema.py @@ -2,12 +2,13 @@ import re import typing from collections import OrderedDict -from drf_spectacular.extensions import OpenApiSerializerFieldExtension +from drf_spectacular.extensions import OpenApiSerializerFieldExtension, OpenApiSerializerExtension, _SchemaType from drf_spectacular.openapi import AutoSchema from drf_spectacular.plumbing import ( build_basic_type, build_choice_field, build_media_type_object, build_object_type, get_doc, ) from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import Direction from netbox.api.fields import ChoiceField from netbox.api.serializers import WritableNestedSerializer @@ -277,3 +278,20 @@ class FixSerializedPKRelatedField(OpenApiSerializerFieldExtension): return component.ref if component else None else: return build_basic_type(OpenApiTypes.INT) + + +class FixIntegerRangeSerializerSchema(OpenApiSerializerExtension): + target_class = 'netbox.api.fields.IntegerRangeSerializer' + + def map_serializer(self, auto_schema: 'AutoSchema', direction: Direction) -> _SchemaType: + return { + 'type': 'array', + 'items': { + 'type': 'array', + 'items': { + 'type': 'integer', + }, + 'minItems': 2, + 'maxItems': 2, + }, + } From 787a2dd7c280bd8cb642613efe0240fe40490d5c Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 26 Feb 2025 05:02:07 +0000 Subject: [PATCH 14/38] Update source translation strings --- netbox/translations/en/LC_MESSAGES/django.po | 712 +++++++++---------- 1 file changed, 356 insertions(+), 356 deletions(-) diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index 650140c41..7f6aef08a 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-02-25 05:01+0000\n" +"POT-Creation-Date: 2025-02-26 05:01+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -162,8 +162,8 @@ msgstr "" #: netbox/dcim/filtersets.py:99 netbox/dcim/filtersets.py:153 #: netbox/dcim/filtersets.py:213 netbox/dcim/filtersets.py:334 #: netbox/dcim/filtersets.py:465 netbox/dcim/filtersets.py:1022 -#: netbox/dcim/filtersets.py:1369 netbox/dcim/filtersets.py:2026 -#: netbox/dcim/filtersets.py:2269 netbox/dcim/filtersets.py:2327 +#: netbox/dcim/filtersets.py:1370 netbox/dcim/filtersets.py:2027 +#: netbox/dcim/filtersets.py:2270 netbox/dcim/filtersets.py:2328 #: netbox/ipam/filtersets.py:928 netbox/virtualization/filtersets.py:139 #: netbox/vpn/filtersets.py:358 msgid "Region (ID)" @@ -174,8 +174,8 @@ msgstr "" #: netbox/dcim/filtersets.py:106 netbox/dcim/filtersets.py:159 #: netbox/dcim/filtersets.py:220 netbox/dcim/filtersets.py:341 #: netbox/dcim/filtersets.py:472 netbox/dcim/filtersets.py:1029 -#: netbox/dcim/filtersets.py:1376 netbox/dcim/filtersets.py:2033 -#: netbox/dcim/filtersets.py:2276 netbox/dcim/filtersets.py:2334 +#: netbox/dcim/filtersets.py:1377 netbox/dcim/filtersets.py:2034 +#: netbox/dcim/filtersets.py:2277 netbox/dcim/filtersets.py:2335 #: netbox/extras/filtersets.py:509 netbox/ipam/filtersets.py:935 #: netbox/virtualization/filtersets.py:146 netbox/vpn/filtersets.py:353 msgid "Region (slug)" @@ -185,9 +185,9 @@ msgstr "" #: netbox/circuits/filtersets.py:297 netbox/dcim/base_filtersets.py:35 #: netbox/dcim/filtersets.py:129 netbox/dcim/filtersets.py:226 #: netbox/dcim/filtersets.py:347 netbox/dcim/filtersets.py:478 -#: netbox/dcim/filtersets.py:1035 netbox/dcim/filtersets.py:1382 -#: netbox/dcim/filtersets.py:2039 netbox/dcim/filtersets.py:2282 -#: netbox/dcim/filtersets.py:2340 netbox/ipam/filtersets.py:941 +#: netbox/dcim/filtersets.py:1035 netbox/dcim/filtersets.py:1383 +#: netbox/dcim/filtersets.py:2040 netbox/dcim/filtersets.py:2283 +#: netbox/dcim/filtersets.py:2341 netbox/ipam/filtersets.py:941 #: netbox/virtualization/filtersets.py:152 msgid "Site group (ID)" msgstr "" @@ -196,9 +196,9 @@ msgstr "" #: netbox/circuits/filtersets.py:304 netbox/dcim/base_filtersets.py:42 #: netbox/dcim/filtersets.py:136 netbox/dcim/filtersets.py:233 #: netbox/dcim/filtersets.py:354 netbox/dcim/filtersets.py:485 -#: netbox/dcim/filtersets.py:1042 netbox/dcim/filtersets.py:1389 -#: netbox/dcim/filtersets.py:2046 netbox/dcim/filtersets.py:2289 -#: netbox/dcim/filtersets.py:2347 netbox/extras/filtersets.py:515 +#: netbox/dcim/filtersets.py:1042 netbox/dcim/filtersets.py:1390 +#: netbox/dcim/filtersets.py:2047 netbox/dcim/filtersets.py:2290 +#: netbox/dcim/filtersets.py:2348 netbox/extras/filtersets.py:515 #: netbox/ipam/filtersets.py:948 netbox/virtualization/filtersets.py:159 msgid "Site group (slug)" msgstr "" @@ -220,14 +220,14 @@ msgstr "" #: netbox/dcim/forms/filtersets.py:1670 netbox/dcim/forms/model_forms.py:141 #: netbox/dcim/forms/model_forms.py:169 netbox/dcim/forms/model_forms.py:243 #: netbox/dcim/forms/model_forms.py:473 netbox/dcim/forms/model_forms.py:734 -#: netbox/dcim/forms/object_create.py:383 netbox/dcim/tables/devices.py:163 +#: netbox/dcim/forms/object_create.py:383 netbox/dcim/tables/devices.py:164 #: netbox/dcim/tables/power.py:26 netbox/dcim/tables/power.py:93 #: netbox/dcim/tables/racks.py:121 netbox/dcim/tables/racks.py:206 #: netbox/dcim/tables/sites.py:133 netbox/extras/filtersets.py:525 #: netbox/ipam/forms/bulk_edit.py:468 netbox/ipam/forms/bulk_import.py:452 #: netbox/ipam/forms/filtersets.py:155 netbox/ipam/forms/filtersets.py:229 #: netbox/ipam/forms/filtersets.py:435 netbox/ipam/forms/filtersets.py:530 -#: netbox/ipam/forms/model_forms.py:671 netbox/ipam/tables/vlans.py:87 +#: netbox/ipam/forms/model_forms.py:679 netbox/ipam/tables/vlans.py:87 #: netbox/ipam/tables/vlans.py:197 netbox/templates/dcim/device.html:22 #: netbox/templates/dcim/inc/cable_termination.html:8 #: netbox/templates/dcim/inc/cable_termination.html:33 @@ -311,9 +311,9 @@ msgstr "" #: netbox/circuits/filtersets.py:229 netbox/circuits/filtersets.py:309 #: netbox/dcim/base_filtersets.py:47 netbox/dcim/filtersets.py:237 #: netbox/dcim/filtersets.py:358 netbox/dcim/filtersets.py:453 -#: netbox/dcim/filtersets.py:1046 netbox/dcim/filtersets.py:1394 -#: netbox/dcim/filtersets.py:2051 netbox/dcim/filtersets.py:2293 -#: netbox/dcim/filtersets.py:2352 netbox/ipam/filtersets.py:237 +#: netbox/dcim/filtersets.py:1046 netbox/dcim/filtersets.py:1395 +#: netbox/dcim/filtersets.py:2052 netbox/dcim/filtersets.py:2294 +#: netbox/dcim/filtersets.py:2353 netbox/ipam/filtersets.py:237 #: netbox/ipam/filtersets.py:952 netbox/virtualization/filtersets.py:163 #: netbox/vpn/filtersets.py:368 msgid "Site (ID)" @@ -322,8 +322,8 @@ msgstr "" #: netbox/circuits/filtersets.py:239 netbox/circuits/filtersets.py:321 #: netbox/dcim/base_filtersets.py:59 netbox/dcim/filtersets.py:259 #: netbox/dcim/filtersets.py:370 netbox/dcim/filtersets.py:491 -#: netbox/dcim/filtersets.py:1058 netbox/dcim/filtersets.py:1405 -#: netbox/dcim/filtersets.py:2305 +#: netbox/dcim/filtersets.py:1058 netbox/dcim/filtersets.py:1406 +#: netbox/dcim/filtersets.py:2306 msgid "Location (ID)" msgstr "" @@ -334,15 +334,15 @@ msgstr "" #: netbox/circuits/filtersets.py:273 netbox/circuits/filtersets.py:375 #: netbox/circuits/filtersets.py:537 netbox/core/filtersets.py:77 #: netbox/core/filtersets.py:136 netbox/core/filtersets.py:173 -#: netbox/dcim/filtersets.py:752 netbox/dcim/filtersets.py:1363 -#: netbox/dcim/filtersets.py:2400 netbox/extras/filtersets.py:41 +#: netbox/dcim/filtersets.py:752 netbox/dcim/filtersets.py:1364 +#: netbox/dcim/filtersets.py:2401 netbox/extras/filtersets.py:41 #: netbox/extras/filtersets.py:63 netbox/extras/filtersets.py:92 #: netbox/extras/filtersets.py:132 netbox/extras/filtersets.py:181 #: netbox/extras/filtersets.py:209 netbox/extras/filtersets.py:239 #: netbox/extras/filtersets.py:276 netbox/extras/filtersets.py:348 #: netbox/extras/filtersets.py:391 netbox/extras/filtersets.py:438 #: netbox/extras/filtersets.py:498 netbox/extras/filtersets.py:657 -#: netbox/extras/filtersets.py:703 netbox/ipam/forms/model_forms.py:484 +#: netbox/extras/filtersets.py:703 netbox/ipam/forms/model_forms.py:492 #: netbox/netbox/filtersets.py:286 netbox/netbox/forms/__init__.py:22 #: netbox/netbox/forms/base.py:167 #: netbox/templates/htmx/object_selector.html:28 @@ -377,7 +377,7 @@ msgstr "" #: netbox/circuits/filtersets.py:328 netbox/dcim/base_filtersets.py:66 #: netbox/dcim/filtersets.py:266 netbox/dcim/filtersets.py:377 -#: netbox/dcim/filtersets.py:498 netbox/dcim/filtersets.py:1411 +#: netbox/dcim/filtersets.py:498 netbox/dcim/filtersets.py:1412 #: netbox/extras/filtersets.py:542 msgid "Location (slug)" msgstr "" @@ -398,7 +398,7 @@ msgstr "" msgid "Virtual circuit (CID)" msgstr "" -#: netbox/circuits/filtersets.py:396 netbox/dcim/filtersets.py:1848 +#: netbox/circuits/filtersets.py:396 netbox/dcim/filtersets.py:1849 msgid "Virtual circuit (ID)" msgstr "" @@ -434,8 +434,8 @@ msgstr "" msgid "Virtual circuit" msgstr "" -#: netbox/circuits/filtersets.py:577 netbox/dcim/filtersets.py:1268 -#: netbox/dcim/filtersets.py:1633 netbox/ipam/filtersets.py:601 +#: netbox/circuits/filtersets.py:577 netbox/dcim/filtersets.py:1269 +#: netbox/dcim/filtersets.py:1634 netbox/ipam/filtersets.py:601 #: netbox/vpn/filtersets.py:102 netbox/vpn/filtersets.py:401 msgid "Interface (ID)" msgstr "" @@ -643,8 +643,8 @@ msgstr "" #: netbox/dcim/forms/bulk_edit.py:1191 netbox/dcim/forms/bulk_edit.py:1218 #: netbox/dcim/forms/bulk_edit.py:1742 netbox/dcim/forms/filtersets.py:1065 #: netbox/dcim/forms/filtersets.py:1323 netbox/dcim/forms/filtersets.py:1460 -#: netbox/dcim/forms/filtersets.py:1484 netbox/dcim/tables/devices.py:737 -#: netbox/dcim/tables/devices.py:793 netbox/dcim/tables/devices.py:1034 +#: netbox/dcim/forms/filtersets.py:1484 netbox/dcim/tables/devices.py:738 +#: netbox/dcim/tables/devices.py:794 netbox/dcim/tables/devices.py:1035 #: netbox/dcim/tables/devicetypes.py:256 netbox/dcim/tables/devicetypes.py:271 #: netbox/dcim/tables/racks.py:33 netbox/extras/forms/bulk_edit.py:270 #: netbox/extras/tables/tables.py:443 @@ -687,8 +687,8 @@ msgstr "" #: netbox/dcim/forms/filtersets.py:1479 netbox/dcim/forms/model_forms.py:714 #: netbox/dcim/forms/model_forms.py:720 netbox/dcim/forms/object_import.py:84 #: netbox/dcim/forms/object_import.py:113 -#: netbox/dcim/forms/object_import.py:146 netbox/dcim/tables/devices.py:188 -#: netbox/dcim/tables/devices.py:845 netbox/dcim/tables/power.py:77 +#: netbox/dcim/forms/object_import.py:146 netbox/dcim/tables/devices.py:189 +#: netbox/dcim/tables/devices.py:846 netbox/dcim/tables/power.py:77 #: netbox/dcim/tables/racks.py:137 netbox/extras/forms/bulk_import.py:42 #: netbox/extras/tables/tables.py:405 netbox/extras/tables/tables.py:465 #: netbox/netbox/tables/tables.py:243 netbox/templates/circuits/circuit.html:30 @@ -755,9 +755,9 @@ msgstr "" #: netbox/dcim/forms/filtersets.py:360 netbox/dcim/forms/filtersets.py:800 #: netbox/dcim/forms/filtersets.py:925 netbox/dcim/forms/filtersets.py:959 #: netbox/dcim/forms/filtersets.py:1060 netbox/dcim/forms/filtersets.py:1171 -#: netbox/dcim/forms/filtersets.py:1562 netbox/dcim/tables/devices.py:150 -#: netbox/dcim/tables/devices.py:848 netbox/dcim/tables/devices.py:982 -#: netbox/dcim/tables/devices.py:1094 netbox/dcim/tables/modules.py:70 +#: netbox/dcim/forms/filtersets.py:1562 netbox/dcim/tables/devices.py:151 +#: netbox/dcim/tables/devices.py:849 netbox/dcim/tables/devices.py:983 +#: netbox/dcim/tables/devices.py:1095 netbox/dcim/tables/modules.py:70 #: netbox/dcim/tables/power.py:74 netbox/dcim/tables/racks.py:125 #: netbox/dcim/tables/sites.py:82 netbox/dcim/tables/sites.py:137 #: netbox/ipam/forms/bulk_edit.py:240 netbox/ipam/forms/bulk_edit.py:290 @@ -766,7 +766,7 @@ msgstr "" #: netbox/ipam/forms/bulk_import.py:292 netbox/ipam/forms/bulk_import.py:473 #: netbox/ipam/forms/filtersets.py:212 netbox/ipam/forms/filtersets.py:284 #: netbox/ipam/forms/filtersets.py:358 netbox/ipam/forms/filtersets.py:542 -#: netbox/ipam/forms/model_forms.py:503 netbox/ipam/tables/ip.py:183 +#: netbox/ipam/forms/model_forms.py:511 netbox/ipam/tables/ip.py:183 #: netbox/ipam/tables/ip.py:264 netbox/ipam/tables/ip.py:315 #: netbox/ipam/tables/ip.py:378 netbox/ipam/tables/ip.py:405 #: netbox/ipam/tables/vlans.py:95 netbox/ipam/tables/vlans.py:208 @@ -973,7 +973,7 @@ msgstr "" #: netbox/ipam/forms/model_forms.py:67 netbox/ipam/forms/model_forms.py:84 #: netbox/ipam/forms/model_forms.py:119 netbox/ipam/forms/model_forms.py:141 #: netbox/ipam/forms/model_forms.py:166 netbox/ipam/forms/model_forms.py:233 -#: netbox/ipam/forms/model_forms.py:263 netbox/ipam/forms/model_forms.py:322 +#: netbox/ipam/forms/model_forms.py:271 netbox/ipam/forms/model_forms.py:330 #: netbox/netbox/navigation/menu.py:24 #: netbox/templates/dcim/device_edit.html:85 #: netbox/templates/dcim/htmx/cable_edit.html:72 @@ -1068,8 +1068,8 @@ msgstr "" #: netbox/dcim/forms/filtersets.py:368 netbox/dcim/forms/filtersets.py:778 #: netbox/dcim/forms/filtersets.py:1539 netbox/dcim/forms/model_forms.py:256 #: netbox/dcim/forms/model_forms.py:1090 netbox/dcim/forms/model_forms.py:1559 -#: netbox/dcim/forms/object_import.py:182 netbox/dcim/tables/devices.py:179 -#: netbox/dcim/tables/devices.py:840 netbox/dcim/tables/devices.py:966 +#: netbox/dcim/forms/object_import.py:182 netbox/dcim/tables/devices.py:180 +#: netbox/dcim/tables/devices.py:841 netbox/dcim/tables/devices.py:967 #: netbox/dcim/tables/devicetypes.py:311 netbox/dcim/tables/racks.py:128 #: netbox/extras/filtersets.py:552 netbox/ipam/forms/bulk_edit.py:245 #: netbox/ipam/forms/bulk_edit.py:295 netbox/ipam/forms/bulk_edit.py:343 @@ -1078,8 +1078,8 @@ msgstr "" #: netbox/ipam/forms/bulk_import.py:478 netbox/ipam/forms/filtersets.py:240 #: netbox/ipam/forms/filtersets.py:292 netbox/ipam/forms/filtersets.py:363 #: netbox/ipam/forms/filtersets.py:550 netbox/ipam/forms/model_forms.py:194 -#: netbox/ipam/forms/model_forms.py:220 netbox/ipam/forms/model_forms.py:251 -#: netbox/ipam/forms/model_forms.py:678 netbox/ipam/tables/ip.py:209 +#: netbox/ipam/forms/model_forms.py:220 netbox/ipam/forms/model_forms.py:259 +#: netbox/ipam/forms/model_forms.py:686 netbox/ipam/tables/ip.py:209 #: netbox/ipam/tables/ip.py:268 netbox/ipam/tables/ip.py:319 #: netbox/ipam/tables/vlans.py:99 netbox/ipam/tables/vlans.py:211 #: netbox/templates/circuits/virtualcircuittermination.html:42 @@ -1189,9 +1189,9 @@ msgstr "" #: netbox/dcim/forms/bulk_import.py:1219 netbox/dcim/forms/model_forms.py:1164 #: netbox/dcim/forms/model_forms.py:1433 netbox/dcim/forms/model_forms.py:1600 #: netbox/dcim/forms/model_forms.py:1635 netbox/dcim/forms/model_forms.py:1765 -#: netbox/dcim/tables/connections.py:65 netbox/dcim/tables/devices.py:1140 -#: netbox/ipam/forms/bulk_import.py:317 netbox/ipam/forms/model_forms.py:282 -#: netbox/ipam/forms/model_forms.py:291 netbox/ipam/tables/fhrp.py:64 +#: netbox/dcim/tables/connections.py:65 netbox/dcim/tables/devices.py:1141 +#: netbox/ipam/forms/bulk_import.py:317 netbox/ipam/forms/model_forms.py:290 +#: netbox/ipam/forms/model_forms.py:299 netbox/ipam/tables/fhrp.py:64 #: netbox/ipam/tables/ip.py:324 netbox/ipam/tables/vlans.py:145 #: netbox/templates/circuits/inc/circuit_termination_fields.html:52 #: netbox/templates/circuits/virtualcircuittermination.html:53 @@ -1236,7 +1236,7 @@ msgstr "" #: netbox/dcim/forms/filtersets.py:1513 netbox/dcim/forms/filtersets.py:1530 #: netbox/dcim/forms/model_forms.py:184 netbox/dcim/forms/model_forms.py:248 #: netbox/dcim/forms/model_forms.py:478 netbox/dcim/forms/model_forms.py:739 -#: netbox/dcim/tables/devices.py:167 netbox/dcim/tables/power.py:30 +#: netbox/dcim/tables/devices.py:168 netbox/dcim/tables/power.py:30 #: netbox/dcim/tables/racks.py:117 netbox/dcim/tables/racks.py:211 #: netbox/extras/filtersets.py:536 netbox/extras/forms/filtersets.py:327 #: netbox/ipam/forms/filtersets.py:234 netbox/ipam/forms/filtersets.py:417 @@ -1283,7 +1283,7 @@ msgstr "" #: netbox/dcim/forms/filtersets.py:1137 netbox/dcim/forms/filtersets.py:1614 #: netbox/dcim/forms/filtersets.py:1638 netbox/dcim/forms/filtersets.py:1662 #: netbox/dcim/forms/model_forms.py:114 netbox/dcim/forms/object_create.py:367 -#: netbox/dcim/tables/devices.py:153 netbox/dcim/tables/sites.py:85 +#: netbox/dcim/tables/devices.py:154 netbox/dcim/tables/sites.py:85 #: netbox/extras/filtersets.py:503 netbox/ipam/forms/bulk_edit.py:458 #: netbox/ipam/forms/filtersets.py:219 netbox/ipam/forms/filtersets.py:425 #: netbox/ipam/forms/filtersets.py:516 netbox/templates/dcim/device.html:18 @@ -1330,8 +1330,8 @@ msgid "Term Side" msgstr "" #: netbox/circuits/forms/filtersets.py:286 netbox/dcim/forms/bulk_edit.py:1572 -#: netbox/extras/forms/model_forms.py:582 netbox/ipam/forms/filtersets.py:144 -#: netbox/ipam/forms/filtersets.py:598 netbox/ipam/forms/model_forms.py:329 +#: netbox/extras/forms/model_forms.py:596 netbox/ipam/forms/filtersets.py:144 +#: netbox/ipam/forms/filtersets.py:598 netbox/ipam/forms/model_forms.py:337 #: netbox/templates/dcim/macaddress.html:25 #: netbox/templates/extras/configcontext.html:60 #: netbox/templates/ipam/ipaddress.html:59 @@ -1346,7 +1346,7 @@ msgstr "" #: netbox/dcim/forms/bulk_import.py:102 netbox/dcim/forms/model_forms.py:120 #: netbox/dcim/tables/sites.py:89 netbox/extras/forms/filtersets.py:489 #: netbox/ipam/filtersets.py:968 netbox/ipam/forms/bulk_edit.py:477 -#: netbox/ipam/forms/bulk_import.py:459 netbox/ipam/forms/model_forms.py:563 +#: netbox/ipam/forms/bulk_import.py:459 netbox/ipam/forms/model_forms.py:571 #: netbox/ipam/tables/fhrp.py:67 netbox/ipam/tables/vlans.py:91 #: netbox/ipam/tables/vlans.py:202 #: netbox/templates/circuits/circuitgroupassignment.html:22 @@ -1431,8 +1431,8 @@ msgstr "" #: netbox/circuits/models/virtual_circuits.py:59 netbox/core/models/data.py:52 #: netbox/core/models/jobs.py:85 netbox/dcim/models/cables.py:49 #: netbox/dcim/models/device_components.py:1281 -#: netbox/dcim/models/devices.py:645 netbox/dcim/models/devices.py:1177 -#: netbox/dcim/models/devices.py:1405 netbox/dcim/models/power.py:94 +#: netbox/dcim/models/devices.py:645 netbox/dcim/models/devices.py:1181 +#: netbox/dcim/models/devices.py:1409 netbox/dcim/models/power.py:94 #: netbox/dcim/models/racks.py:288 netbox/dcim/models/sites.py:154 #: netbox/dcim/models/sites.py:270 netbox/ipam/models/ip.py:237 #: netbox/ipam/models/ip.py:508 netbox/ipam/models/ip.py:729 @@ -1563,7 +1563,7 @@ msgstr "" #: netbox/core/models/jobs.py:46 #: netbox/dcim/models/device_component_templates.py:43 #: netbox/dcim/models/device_components.py:52 netbox/dcim/models/devices.py:589 -#: netbox/dcim/models/devices.py:1337 netbox/dcim/models/devices.py:1400 +#: netbox/dcim/models/devices.py:1341 netbox/dcim/models/devices.py:1404 #: netbox/dcim/models/power.py:38 netbox/dcim/models/power.py:89 #: netbox/dcim/models/racks.py:257 netbox/dcim/models/sites.py:142 #: netbox/extras/models/configs.py:36 netbox/extras/models/configs.py:215 @@ -1675,14 +1675,14 @@ msgstr "" #: netbox/core/tables/tasks.py:11 netbox/core/tables/tasks.py:115 #: netbox/dcim/forms/filtersets.py:64 netbox/dcim/forms/object_create.py:43 #: netbox/dcim/tables/devices.py:63 netbox/dcim/tables/devices.py:103 -#: netbox/dcim/tables/devices.py:145 netbox/dcim/tables/devices.py:299 -#: netbox/dcim/tables/devices.py:402 netbox/dcim/tables/devices.py:443 -#: netbox/dcim/tables/devices.py:491 netbox/dcim/tables/devices.py:540 -#: netbox/dcim/tables/devices.py:561 netbox/dcim/tables/devices.py:681 -#: netbox/dcim/tables/devices.py:764 netbox/dcim/tables/devices.py:810 -#: netbox/dcim/tables/devices.py:872 netbox/dcim/tables/devices.py:941 -#: netbox/dcim/tables/devices.py:1006 netbox/dcim/tables/devices.py:1025 -#: netbox/dcim/tables/devices.py:1054 netbox/dcim/tables/devices.py:1084 +#: netbox/dcim/tables/devices.py:145 netbox/dcim/tables/devices.py:300 +#: netbox/dcim/tables/devices.py:403 netbox/dcim/tables/devices.py:444 +#: netbox/dcim/tables/devices.py:492 netbox/dcim/tables/devices.py:541 +#: netbox/dcim/tables/devices.py:562 netbox/dcim/tables/devices.py:682 +#: netbox/dcim/tables/devices.py:765 netbox/dcim/tables/devices.py:811 +#: netbox/dcim/tables/devices.py:873 netbox/dcim/tables/devices.py:942 +#: netbox/dcim/tables/devices.py:1007 netbox/dcim/tables/devices.py:1026 +#: netbox/dcim/tables/devices.py:1055 netbox/dcim/tables/devices.py:1085 #: netbox/dcim/tables/devicetypes.py:31 netbox/dcim/tables/devicetypes.py:227 #: netbox/dcim/tables/power.py:22 netbox/dcim/tables/power.py:62 #: netbox/dcim/tables/racks.py:24 netbox/dcim/tables/racks.py:113 @@ -1826,7 +1826,7 @@ msgstr "" #: netbox/circuits/tables/providers.py:81 #: netbox/circuits/tables/providers.py:106 #: netbox/circuits/tables/virtual_circuits.py:68 -#: netbox/dcim/tables/devices.py:1067 netbox/dcim/tables/devicetypes.py:97 +#: netbox/dcim/tables/devices.py:1068 netbox/dcim/tables/devicetypes.py:97 #: netbox/dcim/tables/modules.py:29 netbox/dcim/tables/modules.py:73 #: netbox/dcim/tables/power.py:39 netbox/dcim/tables/power.py:96 #: netbox/dcim/tables/racks.py:84 netbox/dcim/tables/racks.py:144 @@ -1870,7 +1870,7 @@ msgstr "" msgid "Termination Point" msgstr "" -#: netbox/circuits/tables/circuits.py:134 netbox/dcim/tables/devices.py:160 +#: netbox/circuits/tables/circuits.py:134 netbox/dcim/tables/devices.py:161 #: netbox/templates/dcim/sitegroup.html:26 msgid "Site Group" msgstr "" @@ -1925,17 +1925,17 @@ msgstr "" #: netbox/dcim/forms/model_forms.py:1231 netbox/dcim/forms/model_forms.py:1716 #: netbox/dcim/forms/model_forms.py:1787 netbox/dcim/forms/object_create.py:249 #: netbox/dcim/tables/connections.py:22 netbox/dcim/tables/connections.py:41 -#: netbox/dcim/tables/connections.py:60 netbox/dcim/tables/devices.py:295 -#: netbox/dcim/tables/devices.py:380 netbox/dcim/tables/devices.py:421 -#: netbox/dcim/tables/devices.py:463 netbox/dcim/tables/devices.py:513 -#: netbox/dcim/tables/devices.py:618 netbox/dcim/tables/devices.py:730 -#: netbox/dcim/tables/devices.py:786 netbox/dcim/tables/devices.py:832 -#: netbox/dcim/tables/devices.py:891 netbox/dcim/tables/devices.py:959 -#: netbox/dcim/tables/devices.py:1088 netbox/dcim/tables/modules.py:53 +#: netbox/dcim/tables/connections.py:60 netbox/dcim/tables/devices.py:296 +#: netbox/dcim/tables/devices.py:381 netbox/dcim/tables/devices.py:422 +#: netbox/dcim/tables/devices.py:464 netbox/dcim/tables/devices.py:514 +#: netbox/dcim/tables/devices.py:619 netbox/dcim/tables/devices.py:731 +#: netbox/dcim/tables/devices.py:787 netbox/dcim/tables/devices.py:833 +#: netbox/dcim/tables/devices.py:892 netbox/dcim/tables/devices.py:960 +#: netbox/dcim/tables/devices.py:1089 netbox/dcim/tables/modules.py:53 #: netbox/extras/forms/filtersets.py:328 netbox/ipam/forms/bulk_import.py:303 #: netbox/ipam/forms/bulk_import.py:540 netbox/ipam/forms/filtersets.py:603 -#: netbox/ipam/forms/model_forms.py:325 netbox/ipam/forms/model_forms.py:754 -#: netbox/ipam/forms/model_forms.py:787 netbox/ipam/forms/model_forms.py:813 +#: netbox/ipam/forms/model_forms.py:333 netbox/ipam/forms/model_forms.py:762 +#: netbox/ipam/forms/model_forms.py:795 netbox/ipam/forms/model_forms.py:821 #: netbox/ipam/tables/vlans.py:156 #: netbox/templates/circuits/virtualcircuittermination.html:56 #: netbox/templates/dcim/consoleport.html:20 @@ -2189,7 +2189,7 @@ msgstr "" #: netbox/core/forms/bulk_edit.py:25 netbox/core/forms/filtersets.py:43 #: netbox/core/tables/data.py:26 netbox/dcim/forms/bulk_edit.py:1140 #: netbox/dcim/forms/bulk_edit.py:1418 netbox/dcim/forms/filtersets.py:1375 -#: netbox/dcim/tables/devices.py:566 netbox/dcim/tables/devicetypes.py:231 +#: netbox/dcim/tables/devices.py:567 netbox/dcim/tables/devicetypes.py:231 #: netbox/extras/forms/bulk_edit.py:123 netbox/extras/forms/bulk_edit.py:187 #: netbox/extras/forms/bulk_edit.py:246 netbox/extras/forms/filtersets.py:145 #: netbox/extras/forms/filtersets.py:235 netbox/extras/forms/filtersets.py:300 @@ -2208,7 +2208,7 @@ msgstr "" msgid "Enabled" msgstr "" -#: netbox/core/forms/bulk_edit.py:34 netbox/extras/forms/model_forms.py:285 +#: netbox/core/forms/bulk_edit.py:34 netbox/extras/forms/model_forms.py:299 #: netbox/templates/extras/savedfilter.html:52 #: netbox/vpn/forms/filtersets.py:97 netbox/vpn/forms/filtersets.py:127 #: netbox/vpn/forms/filtersets.py:151 netbox/vpn/forms/filtersets.py:170 @@ -2223,9 +2223,9 @@ msgid "Ignore rules" msgstr "" #: netbox/core/forms/filtersets.py:30 netbox/core/forms/model_forms.py:97 -#: netbox/extras/forms/model_forms.py:248 -#: netbox/extras/forms/model_forms.py:578 -#: netbox/extras/forms/model_forms.py:632 netbox/extras/tables/tables.py:191 +#: netbox/extras/forms/model_forms.py:262 +#: netbox/extras/forms/model_forms.py:592 +#: netbox/extras/forms/model_forms.py:646 netbox/extras/tables/tables.py:191 #: netbox/extras/tables/tables.py:483 netbox/extras/tables/tables.py:518 #: netbox/templates/core/datasource.html:31 #: netbox/templates/extras/configcontext.html:29 @@ -2322,7 +2322,7 @@ msgid "Before" msgstr "" #: netbox/core/forms/filtersets.py:149 netbox/core/tables/change_logging.py:29 -#: netbox/extras/forms/model_forms.py:396 +#: netbox/extras/forms/model_forms.py:410 #: netbox/templates/core/objectchange.html:46 #: netbox/templates/extras/eventrule.html:71 msgid "Action" @@ -3015,8 +3015,8 @@ msgstr "" #: netbox/dcim/forms/model_forms.py:76 netbox/dcim/forms/model_forms.py:95 #: netbox/dcim/forms/model_forms.py:174 netbox/dcim/forms/model_forms.py:1082 #: netbox/dcim/forms/model_forms.py:1551 netbox/dcim/forms/object_import.py:177 -#: netbox/dcim/tables/devices.py:689 netbox/dcim/tables/devices.py:899 -#: netbox/dcim/tables/devices.py:986 netbox/dcim/tables/devices.py:1146 +#: netbox/dcim/tables/devices.py:690 netbox/dcim/tables/devices.py:900 +#: netbox/dcim/tables/devices.py:987 netbox/dcim/tables/devices.py:1147 #: netbox/extras/tables/tables.py:223 netbox/ipam/tables/fhrp.py:59 #: netbox/ipam/tables/ip.py:330 netbox/ipam/tables/services.py:44 #: netbox/templates/dcim/interface.html:108 @@ -3155,7 +3155,7 @@ msgstr "" #: netbox/dcim/choices.py:1025 netbox/dcim/forms/bulk_edit.py:1431 #: netbox/dcim/forms/bulk_import.py:870 netbox/dcim/forms/model_forms.py:993 -#: netbox/dcim/tables/devices.py:693 netbox/templates/dcim/interface.html:112 +#: netbox/dcim/tables/devices.py:694 netbox/templates/dcim/interface.html:112 #: netbox/templates/virtualization/vminterface.html:43 #: netbox/virtualization/forms/bulk_edit.py:194 #: netbox/virtualization/forms/bulk_import.py:164 @@ -3354,16 +3354,16 @@ msgstr "" #: netbox/dcim/filtersets.py:297 netbox/dcim/filtersets.py:382 #: netbox/dcim/filtersets.py:540 netbox/dcim/filtersets.py:679 #: netbox/dcim/filtersets.py:883 netbox/dcim/filtersets.py:934 -#: netbox/dcim/filtersets.py:974 netbox/dcim/filtersets.py:1307 -#: netbox/dcim/filtersets.py:1959 +#: netbox/dcim/filtersets.py:974 netbox/dcim/filtersets.py:1308 +#: netbox/dcim/filtersets.py:1960 msgid "Manufacturer (ID)" msgstr "" #: netbox/dcim/filtersets.py:303 netbox/dcim/filtersets.py:388 #: netbox/dcim/filtersets.py:546 netbox/dcim/filtersets.py:685 #: netbox/dcim/filtersets.py:889 netbox/dcim/filtersets.py:940 -#: netbox/dcim/filtersets.py:980 netbox/dcim/filtersets.py:1313 -#: netbox/dcim/filtersets.py:1965 +#: netbox/dcim/filtersets.py:980 netbox/dcim/filtersets.py:1314 +#: netbox/dcim/filtersets.py:1966 msgid "Manufacturer (slug)" msgstr "" @@ -3376,14 +3376,14 @@ msgid "Rack type (ID)" msgstr "" #: netbox/dcim/filtersets.py:412 netbox/dcim/filtersets.py:893 -#: netbox/dcim/filtersets.py:995 netbox/dcim/filtersets.py:1969 +#: netbox/dcim/filtersets.py:995 netbox/dcim/filtersets.py:1970 #: netbox/ipam/filtersets.py:350 netbox/ipam/filtersets.py:462 #: netbox/ipam/filtersets.py:972 netbox/virtualization/filtersets.py:176 msgid "Role (ID)" msgstr "" #: netbox/dcim/filtersets.py:418 netbox/dcim/filtersets.py:899 -#: netbox/dcim/filtersets.py:1001 netbox/dcim/filtersets.py:1975 +#: netbox/dcim/filtersets.py:1001 netbox/dcim/filtersets.py:1976 #: netbox/extras/filtersets.py:558 netbox/ipam/filtersets.py:356 #: netbox/ipam/filtersets.py:468 netbox/ipam/filtersets.py:978 #: netbox/virtualization/filtersets.py:182 @@ -3391,7 +3391,7 @@ msgid "Role (slug)" msgstr "" #: netbox/dcim/filtersets.py:448 netbox/dcim/filtersets.py:1063 -#: netbox/dcim/filtersets.py:1416 netbox/dcim/filtersets.py:2367 +#: netbox/dcim/filtersets.py:1417 netbox/dcim/filtersets.py:2368 msgid "Rack (ID)" msgstr "" @@ -3469,19 +3469,19 @@ msgid "Has inventory items" msgstr "" #: netbox/dcim/filtersets.py:757 netbox/dcim/filtersets.py:990 -#: netbox/dcim/filtersets.py:1437 +#: netbox/dcim/filtersets.py:1438 msgid "Device type (ID)" msgstr "" -#: netbox/dcim/filtersets.py:773 netbox/dcim/filtersets.py:1318 +#: netbox/dcim/filtersets.py:773 netbox/dcim/filtersets.py:1319 msgid "Module type (ID)" msgstr "" -#: netbox/dcim/filtersets.py:805 netbox/dcim/filtersets.py:1592 +#: netbox/dcim/filtersets.py:805 netbox/dcim/filtersets.py:1593 msgid "Power port (ID)" msgstr "" -#: netbox/dcim/filtersets.py:879 netbox/dcim/filtersets.py:1955 +#: netbox/dcim/filtersets.py:879 netbox/dcim/filtersets.py:1956 msgid "Parent inventory item (ID)" msgstr "" @@ -3507,9 +3507,9 @@ msgstr "" msgid "Platform (slug)" msgstr "" -#: netbox/dcim/filtersets.py:1052 netbox/dcim/filtersets.py:1400 -#: netbox/dcim/filtersets.py:2057 netbox/dcim/filtersets.py:2299 -#: netbox/dcim/filtersets.py:2358 +#: netbox/dcim/filtersets.py:1052 netbox/dcim/filtersets.py:1401 +#: netbox/dcim/filtersets.py:2058 netbox/dcim/filtersets.py:2300 +#: netbox/dcim/filtersets.py:2359 msgid "Site name (slug)" msgstr "" @@ -3541,7 +3541,7 @@ msgstr "" #: netbox/dcim/filtersets.py:1104 netbox/dcim/forms/filtersets.py:819 #: netbox/dcim/forms/filtersets.py:1390 netbox/dcim/forms/filtersets.py:1586 #: netbox/dcim/forms/filtersets.py:1591 netbox/dcim/forms/model_forms.py:1762 -#: netbox/dcim/models/devices.py:1501 netbox/dcim/models/devices.py:1522 +#: netbox/dcim/models/devices.py:1505 netbox/dcim/models/devices.py:1526 #: netbox/virtualization/filtersets.py:196 #: netbox/virtualization/filtersets.py:268 #: netbox/virtualization/forms/filtersets.py:177 @@ -3549,7 +3549,7 @@ msgstr "" msgid "MAC address" msgstr "" -#: netbox/dcim/filtersets.py:1111 netbox/dcim/filtersets.py:1275 +#: netbox/dcim/filtersets.py:1111 netbox/dcim/filtersets.py:1276 #: netbox/dcim/forms/filtersets.py:828 netbox/dcim/forms/filtersets.py:931 #: netbox/virtualization/filtersets.py:200 #: netbox/virtualization/forms/filtersets.py:181 @@ -3576,57 +3576,57 @@ msgstr "" msgid "Has virtual device context" msgstr "" -#: netbox/dcim/filtersets.py:1258 +#: netbox/dcim/filtersets.py:1259 msgid "VDC (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1263 +#: netbox/dcim/filtersets.py:1264 msgid "Device model" msgstr "" -#: netbox/dcim/filtersets.py:1324 +#: netbox/dcim/filtersets.py:1325 msgid "Module type (model)" msgstr "" -#: netbox/dcim/filtersets.py:1330 +#: netbox/dcim/filtersets.py:1331 msgid "Module bay (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1334 netbox/dcim/filtersets.py:1426 -#: netbox/dcim/filtersets.py:1612 netbox/ipam/filtersets.py:580 +#: netbox/dcim/filtersets.py:1335 netbox/dcim/filtersets.py:1427 +#: netbox/dcim/filtersets.py:1613 netbox/ipam/filtersets.py:580 #: netbox/ipam/filtersets.py:820 netbox/ipam/filtersets.py:1142 #: netbox/virtualization/filtersets.py:127 netbox/vpn/filtersets.py:379 msgid "Device (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1422 +#: netbox/dcim/filtersets.py:1423 msgid "Rack (name)" msgstr "" -#: netbox/dcim/filtersets.py:1432 netbox/dcim/filtersets.py:1607 +#: netbox/dcim/filtersets.py:1433 netbox/dcim/filtersets.py:1608 #: netbox/ipam/filtersets.py:575 netbox/ipam/filtersets.py:815 #: netbox/ipam/filtersets.py:1148 netbox/vpn/filtersets.py:374 msgid "Device (name)" msgstr "" -#: netbox/dcim/filtersets.py:1443 +#: netbox/dcim/filtersets.py:1444 msgid "Device type (model)" msgstr "" -#: netbox/dcim/filtersets.py:1448 +#: netbox/dcim/filtersets.py:1449 msgid "Device role (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1454 +#: netbox/dcim/filtersets.py:1455 msgid "Device role (slug)" msgstr "" -#: netbox/dcim/filtersets.py:1459 +#: netbox/dcim/filtersets.py:1460 msgid "Virtual Chassis (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1465 netbox/dcim/forms/filtersets.py:110 -#: netbox/dcim/tables/devices.py:216 netbox/netbox/navigation/menu.py:79 +#: netbox/dcim/filtersets.py:1466 netbox/dcim/forms/filtersets.py:110 +#: netbox/dcim/tables/devices.py:217 netbox/netbox/navigation/menu.py:79 #: netbox/templates/dcim/device.html:120 #: netbox/templates/dcim/device_edit.html:93 #: netbox/templates/dcim/virtualchassis.html:20 @@ -3635,56 +3635,56 @@ msgstr "" msgid "Virtual Chassis" msgstr "" -#: netbox/dcim/filtersets.py:1489 +#: netbox/dcim/filtersets.py:1490 msgid "Module (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1496 +#: netbox/dcim/filtersets.py:1497 msgid "Cable (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1617 netbox/ipam/filtersets.py:585 +#: netbox/dcim/filtersets.py:1618 netbox/ipam/filtersets.py:585 #: netbox/ipam/filtersets.py:825 netbox/ipam/filtersets.py:1158 #: netbox/vpn/filtersets.py:385 msgid "Virtual machine (name)" msgstr "" -#: netbox/dcim/filtersets.py:1622 netbox/ipam/filtersets.py:590 +#: netbox/dcim/filtersets.py:1623 netbox/ipam/filtersets.py:590 #: netbox/ipam/filtersets.py:830 netbox/ipam/filtersets.py:1152 #: netbox/virtualization/filtersets.py:248 #: netbox/virtualization/filtersets.py:299 netbox/vpn/filtersets.py:390 msgid "Virtual machine (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1628 netbox/ipam/filtersets.py:596 +#: netbox/dcim/filtersets.py:1629 netbox/ipam/filtersets.py:596 #: netbox/vpn/filtersets.py:97 netbox/vpn/filtersets.py:396 msgid "Interface (name)" msgstr "" -#: netbox/dcim/filtersets.py:1639 netbox/ipam/filtersets.py:607 +#: netbox/dcim/filtersets.py:1640 netbox/ipam/filtersets.py:607 #: netbox/vpn/filtersets.py:108 netbox/vpn/filtersets.py:407 msgid "VM interface (name)" msgstr "" -#: netbox/dcim/filtersets.py:1644 netbox/ipam/filtersets.py:612 +#: netbox/dcim/filtersets.py:1645 netbox/ipam/filtersets.py:612 #: netbox/vpn/filtersets.py:113 msgid "VM interface (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1686 netbox/ipam/forms/bulk_import.py:185 +#: netbox/dcim/filtersets.py:1687 netbox/ipam/forms/bulk_import.py:185 #: netbox/vpn/forms/bulk_import.py:308 msgid "Assigned VLAN" msgstr "" -#: netbox/dcim/filtersets.py:1690 +#: netbox/dcim/filtersets.py:1691 msgid "Assigned VID" msgstr "" -#: netbox/dcim/filtersets.py:1695 netbox/dcim/forms/bulk_edit.py:1544 +#: netbox/dcim/filtersets.py:1696 netbox/dcim/forms/bulk_edit.py:1544 #: netbox/dcim/forms/bulk_import.py:921 netbox/dcim/forms/filtersets.py:1433 #: netbox/dcim/forms/model_forms.py:1411 #: netbox/dcim/models/device_components.py:749 -#: netbox/dcim/tables/devices.py:647 netbox/ipam/filtersets.py:321 +#: netbox/dcim/tables/devices.py:648 netbox/ipam/filtersets.py:321 #: netbox/ipam/filtersets.py:332 netbox/ipam/filtersets.py:452 #: netbox/ipam/filtersets.py:553 netbox/ipam/filtersets.py:564 #: netbox/ipam/forms/bulk_edit.py:226 netbox/ipam/forms/bulk_edit.py:282 @@ -3692,9 +3692,9 @@ msgstr "" #: netbox/ipam/forms/bulk_import.py:242 netbox/ipam/forms/bulk_import.py:278 #: netbox/ipam/forms/filtersets.py:69 netbox/ipam/forms/filtersets.py:174 #: netbox/ipam/forms/filtersets.py:312 netbox/ipam/forms/model_forms.py:65 -#: netbox/ipam/forms/model_forms.py:208 netbox/ipam/forms/model_forms.py:248 -#: netbox/ipam/forms/model_forms.py:302 netbox/ipam/forms/model_forms.py:466 -#: netbox/ipam/forms/model_forms.py:480 netbox/ipam/forms/model_forms.py:494 +#: netbox/ipam/forms/model_forms.py:208 netbox/ipam/forms/model_forms.py:256 +#: netbox/ipam/forms/model_forms.py:310 netbox/ipam/forms/model_forms.py:474 +#: netbox/ipam/forms/model_forms.py:488 netbox/ipam/forms/model_forms.py:502 #: netbox/ipam/models/ip.py:217 netbox/ipam/models/ip.py:498 #: netbox/ipam/models/ip.py:719 netbox/ipam/models/vrfs.py:61 #: netbox/ipam/tables/ip.py:188 netbox/ipam/tables/ip.py:261 @@ -3713,19 +3713,19 @@ msgstr "" msgid "VRF" msgstr "" -#: netbox/dcim/filtersets.py:1701 netbox/ipam/filtersets.py:327 +#: netbox/dcim/filtersets.py:1702 netbox/ipam/filtersets.py:327 #: netbox/ipam/filtersets.py:338 netbox/ipam/filtersets.py:458 #: netbox/ipam/filtersets.py:559 netbox/ipam/filtersets.py:570 msgid "VRF (RD)" msgstr "" -#: netbox/dcim/filtersets.py:1706 netbox/ipam/filtersets.py:1010 +#: netbox/dcim/filtersets.py:1707 netbox/ipam/filtersets.py:1010 #: netbox/vpn/filtersets.py:342 msgid "L2VPN (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1712 netbox/dcim/forms/filtersets.py:1438 -#: netbox/dcim/tables/devices.py:583 netbox/ipam/filtersets.py:1016 +#: netbox/dcim/filtersets.py:1713 netbox/dcim/forms/filtersets.py:1438 +#: netbox/dcim/tables/devices.py:584 netbox/ipam/filtersets.py:1016 #: netbox/ipam/forms/filtersets.py:570 netbox/ipam/tables/vlans.py:113 #: netbox/templates/dcim/interface.html:99 netbox/templates/ipam/vlan.html:82 #: netbox/templates/vpn/l2vpntermination.html:12 @@ -3736,128 +3736,128 @@ msgstr "" msgid "L2VPN" msgstr "" -#: netbox/dcim/filtersets.py:1717 netbox/ipam/filtersets.py:1091 +#: netbox/dcim/filtersets.py:1718 netbox/ipam/filtersets.py:1091 msgid "VLAN Translation Policy (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1723 netbox/dcim/forms/model_forms.py:1428 +#: netbox/dcim/filtersets.py:1724 netbox/dcim/forms/model_forms.py:1428 #: netbox/dcim/models/device_components.py:568 -#: netbox/ipam/forms/filtersets.py:489 netbox/ipam/forms/model_forms.py:704 +#: netbox/ipam/forms/filtersets.py:489 netbox/ipam/forms/model_forms.py:712 #: netbox/templates/ipam/vlantranslationpolicy.html:11 #: netbox/virtualization/forms/bulk_edit.py:248 #: netbox/virtualization/forms/model_forms.py:373 msgid "VLAN Translation Policy" msgstr "" -#: netbox/dcim/filtersets.py:1757 +#: netbox/dcim/filtersets.py:1758 msgid "Virtual Chassis Interfaces for Device" msgstr "" -#: netbox/dcim/filtersets.py:1762 +#: netbox/dcim/filtersets.py:1763 msgid "Virtual Chassis Interfaces for Device (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1766 +#: netbox/dcim/filtersets.py:1767 msgid "Kind of interface" msgstr "" -#: netbox/dcim/filtersets.py:1771 netbox/virtualization/filtersets.py:259 +#: netbox/dcim/filtersets.py:1772 netbox/virtualization/filtersets.py:259 msgid "Parent interface (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1776 netbox/virtualization/filtersets.py:264 +#: netbox/dcim/filtersets.py:1777 netbox/virtualization/filtersets.py:264 msgid "Bridged interface (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1781 +#: netbox/dcim/filtersets.py:1782 msgid "LAG interface (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1789 netbox/dcim/tables/devices.py:605 -#: netbox/dcim/tables/devices.py:1135 netbox/templates/dcim/interface.html:131 +#: netbox/dcim/filtersets.py:1790 netbox/dcim/tables/devices.py:606 +#: netbox/dcim/tables/devices.py:1136 netbox/templates/dcim/interface.html:131 #: netbox/templates/dcim/macaddress.html:11 #: netbox/templates/dcim/macaddress.html:14 #: netbox/templates/virtualization/vminterface.html:73 msgid "MAC Address" msgstr "" -#: netbox/dcim/filtersets.py:1794 netbox/virtualization/filtersets.py:273 +#: netbox/dcim/filtersets.py:1795 netbox/virtualization/filtersets.py:273 msgid "Primary MAC address (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1800 netbox/dcim/forms/model_forms.py:1415 +#: netbox/dcim/filtersets.py:1801 netbox/dcim/forms/model_forms.py:1415 #: netbox/virtualization/filtersets.py:279 #: netbox/virtualization/forms/model_forms.py:311 msgid "Primary MAC address" msgstr "" -#: netbox/dcim/filtersets.py:1822 netbox/dcim/filtersets.py:1834 +#: netbox/dcim/filtersets.py:1823 netbox/dcim/filtersets.py:1835 #: netbox/dcim/forms/filtersets.py:1350 netbox/dcim/forms/model_forms.py:1742 #: netbox/templates/dcim/virtualdevicecontext.html:15 msgid "Virtual Device Context" msgstr "" -#: netbox/dcim/filtersets.py:1828 +#: netbox/dcim/filtersets.py:1829 msgid "Virtual Device Context (Identifier)" msgstr "" -#: netbox/dcim/filtersets.py:1839 netbox/templates/wireless/wirelesslan.html:11 +#: netbox/dcim/filtersets.py:1840 netbox/templates/wireless/wirelesslan.html:11 #: netbox/wireless/forms/model_forms.py:55 msgid "Wireless LAN" msgstr "" -#: netbox/dcim/filtersets.py:1843 netbox/dcim/tables/devices.py:634 +#: netbox/dcim/filtersets.py:1844 netbox/dcim/tables/devices.py:635 msgid "Wireless link" msgstr "" -#: netbox/dcim/filtersets.py:1853 +#: netbox/dcim/filtersets.py:1854 msgid "Virtual circuit termination (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1922 +#: netbox/dcim/filtersets.py:1923 msgid "Parent module bay (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1927 +#: netbox/dcim/filtersets.py:1928 msgid "Installed module (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1938 +#: netbox/dcim/filtersets.py:1939 msgid "Installed device (ID)" msgstr "" -#: netbox/dcim/filtersets.py:1944 +#: netbox/dcim/filtersets.py:1945 msgid "Installed device (name)" msgstr "" -#: netbox/dcim/filtersets.py:2014 +#: netbox/dcim/filtersets.py:2015 msgid "Master (ID)" msgstr "" -#: netbox/dcim/filtersets.py:2020 +#: netbox/dcim/filtersets.py:2021 msgid "Master (name)" msgstr "" -#: netbox/dcim/filtersets.py:2062 netbox/tenancy/filtersets.py:245 +#: netbox/dcim/filtersets.py:2063 netbox/tenancy/filtersets.py:245 msgid "Tenant (ID)" msgstr "" -#: netbox/dcim/filtersets.py:2068 netbox/extras/filtersets.py:618 +#: netbox/dcim/filtersets.py:2069 netbox/extras/filtersets.py:618 #: netbox/tenancy/filtersets.py:251 msgid "Tenant (slug)" msgstr "" -#: netbox/dcim/filtersets.py:2104 netbox/dcim/forms/filtersets.py:1078 +#: netbox/dcim/filtersets.py:2105 netbox/dcim/forms/filtersets.py:1078 msgid "Unterminated" msgstr "" -#: netbox/dcim/filtersets.py:2362 +#: netbox/dcim/filtersets.py:2363 msgid "Power panel (ID)" msgstr "" #: netbox/dcim/forms/bulk_create.py:40 netbox/extras/forms/filtersets.py:408 -#: netbox/extras/forms/model_forms.py:567 -#: netbox/extras/forms/model_forms.py:619 netbox/netbox/forms/base.py:86 +#: netbox/extras/forms/model_forms.py:581 +#: netbox/extras/forms/model_forms.py:633 netbox/netbox/forms/base.py:86 #: netbox/netbox/forms/mixins.py:91 netbox/netbox/tables/columns.py:481 #: netbox/templates/circuits/inc/circuit_termination.html:32 #: netbox/templates/generic/bulk_edit.html:65 @@ -3869,8 +3869,8 @@ msgstr "" #: netbox/dcim/forms/bulk_create.py:112 netbox/dcim/forms/filtersets.py:1503 #: netbox/dcim/forms/model_forms.py:498 netbox/dcim/forms/model_forms.py:557 #: netbox/dcim/forms/object_create.py:197 -#: netbox/dcim/forms/object_create.py:345 netbox/dcim/tables/devices.py:175 -#: netbox/dcim/tables/devices.py:740 netbox/dcim/tables/devicetypes.py:253 +#: netbox/dcim/forms/object_create.py:345 netbox/dcim/tables/devices.py:176 +#: netbox/dcim/tables/devices.py:741 netbox/dcim/tables/devicetypes.py:253 #: netbox/templates/dcim/device.html:43 netbox/templates/dcim/device.html:131 #: netbox/templates/dcim/modulebay.html:38 #: netbox/templates/dcim/virtualchassis.html:66 @@ -3916,8 +3916,8 @@ msgstr "" #: netbox/dcim/forms/model_forms.py:357 netbox/dcim/forms/model_forms.py:404 #: netbox/dcim/forms/model_forms.py:445 netbox/dcim/forms/model_forms.py:1095 #: netbox/dcim/forms/model_forms.py:1564 netbox/dcim/forms/object_import.py:188 -#: netbox/dcim/tables/devices.py:107 netbox/dcim/tables/devices.py:182 -#: netbox/dcim/tables/devices.py:969 netbox/dcim/tables/devicetypes.py:85 +#: netbox/dcim/tables/devices.py:107 netbox/dcim/tables/devices.py:183 +#: netbox/dcim/tables/devices.py:970 netbox/dcim/tables/devicetypes.py:85 #: netbox/dcim/tables/devicetypes.py:315 netbox/dcim/tables/modules.py:20 #: netbox/dcim/tables/modules.py:61 netbox/dcim/tables/racks.py:58 #: netbox/dcim/tables/racks.py:131 netbox/templates/dcim/devicetype.html:14 @@ -4077,7 +4077,7 @@ msgstr "" #: netbox/dcim/forms/filtersets.py:1168 netbox/dcim/forms/model_forms.py:271 #: netbox/dcim/forms/model_forms.py:314 netbox/dcim/forms/model_forms.py:489 #: netbox/dcim/forms/model_forms.py:767 netbox/dcim/forms/object_create.py:392 -#: netbox/dcim/tables/devices.py:171 netbox/dcim/tables/power.py:70 +#: netbox/dcim/tables/devices.py:172 netbox/dcim/tables/power.py:70 #: netbox/dcim/tables/racks.py:216 netbox/ipam/forms/filtersets.py:445 #: netbox/templates/dcim/device.html:30 #: netbox/templates/dcim/inc/cable_termination.html:16 @@ -4169,7 +4169,7 @@ msgstr "" #: netbox/dcim/forms/bulk_edit.py:704 netbox/dcim/forms/bulk_import.py:525 #: netbox/dcim/forms/filtersets.py:797 netbox/dcim/forms/model_forms.py:461 -#: netbox/dcim/forms/model_forms.py:524 netbox/dcim/tables/devices.py:192 +#: netbox/dcim/forms/model_forms.py:524 netbox/dcim/tables/devices.py:193 #: netbox/extras/filtersets.py:563 netbox/templates/dcim/device.html:186 #: netbox/templates/dcim/platform.html:26 #: netbox/templates/virtualization/virtualmachine.html:27 @@ -4183,7 +4183,7 @@ msgstr "" #: netbox/dcim/forms/bulk_edit.py:734 netbox/dcim/forms/bulk_import.py:544 #: netbox/dcim/forms/filtersets.py:729 netbox/dcim/forms/filtersets.py:899 -#: netbox/dcim/forms/model_forms.py:533 netbox/dcim/tables/devices.py:212 +#: netbox/dcim/forms/model_forms.py:533 netbox/dcim/tables/devices.py:213 #: netbox/extras/filtersets.py:596 netbox/extras/forms/filtersets.py:329 #: netbox/ipam/forms/filtersets.py:418 netbox/ipam/forms/filtersets.py:450 #: netbox/templates/dcim/device.html:239 @@ -4342,7 +4342,7 @@ msgid "Wireless role" msgstr "" #: netbox/dcim/forms/bulk_edit.py:1306 netbox/dcim/forms/model_forms.py:680 -#: netbox/dcim/forms/model_forms.py:1246 netbox/dcim/tables/devices.py:322 +#: netbox/dcim/forms/model_forms.py:1246 netbox/dcim/tables/devices.py:323 #: netbox/templates/dcim/consoleport.html:24 #: netbox/templates/dcim/consoleserverport.html:24 #: netbox/templates/dcim/frontport.html:24 @@ -4355,7 +4355,7 @@ msgstr "" msgid "Module" msgstr "" -#: netbox/dcim/forms/bulk_edit.py:1445 netbox/dcim/tables/devices.py:698 +#: netbox/dcim/forms/bulk_edit.py:1445 netbox/dcim/tables/devices.py:699 #: netbox/templates/dcim/interface.html:116 msgid "LAG" msgstr "" @@ -4367,7 +4367,7 @@ msgstr "" #: netbox/dcim/forms/bulk_edit.py:1456 netbox/dcim/forms/bulk_import.py:741 #: netbox/dcim/forms/bulk_import.py:767 netbox/dcim/forms/filtersets.py:1253 #: netbox/dcim/forms/filtersets.py:1278 netbox/dcim/forms/filtersets.py:1363 -#: netbox/dcim/tables/devices.py:631 +#: netbox/dcim/tables/devices.py:632 #: netbox/templates/circuits/inc/circuit_termination_fields.html:62 #: netbox/templates/dcim/consoleport.html:40 #: netbox/templates/dcim/consoleserverport.html:40 @@ -4396,14 +4396,14 @@ msgid "VLAN group" msgstr "" #: netbox/dcim/forms/bulk_edit.py:1502 netbox/dcim/forms/model_forms.py:1383 -#: netbox/dcim/tables/devices.py:592 +#: netbox/dcim/tables/devices.py:593 #: netbox/virtualization/forms/bulk_edit.py:230 #: netbox/virtualization/forms/model_forms.py:340 msgid "Untagged VLAN" msgstr "" #: netbox/dcim/forms/bulk_edit.py:1511 netbox/dcim/forms/model_forms.py:1392 -#: netbox/dcim/tables/devices.py:598 +#: netbox/dcim/tables/devices.py:599 #: netbox/virtualization/forms/bulk_edit.py:238 #: netbox/virtualization/forms/model_forms.py:349 msgid "Tagged VLANs" @@ -4427,7 +4427,7 @@ msgid "Wireless LAN group" msgstr "" #: netbox/dcim/forms/bulk_edit.py:1554 netbox/dcim/forms/model_forms.py:1369 -#: netbox/dcim/tables/devices.py:640 netbox/netbox/navigation/menu.py:152 +#: netbox/dcim/tables/devices.py:641 netbox/netbox/navigation/menu.py:152 #: netbox/templates/dcim/interface.html:337 #: netbox/wireless/tables/wirelesslan.py:24 msgid "Wireless LANs" @@ -4803,7 +4803,7 @@ msgstr "" msgid "Physical medium classification" msgstr "" -#: netbox/dcim/forms/bulk_import.py:1058 netbox/dcim/tables/devices.py:853 +#: netbox/dcim/forms/bulk_import.py:1058 netbox/dcim/tables/devices.py:854 msgid "Installed device" msgstr "" @@ -4845,7 +4845,7 @@ msgid "Parent device of assigned interface (if any)" msgstr "" #: netbox/dcim/forms/bulk_import.py:1212 netbox/ipam/forms/bulk_import.py:310 -#: netbox/ipam/forms/bulk_import.py:547 netbox/ipam/forms/model_forms.py:760 +#: netbox/ipam/forms/bulk_import.py:547 netbox/ipam/forms/model_forms.py:768 #: netbox/virtualization/filtersets.py:254 #: netbox/virtualization/filtersets.py:305 #: netbox/virtualization/forms/bulk_edit.py:182 @@ -4928,7 +4928,7 @@ msgid "{side_upper} side termination not found: {device} {name}" msgstr "" #: netbox/dcim/forms/bulk_import.py:1412 netbox/dcim/forms/model_forms.py:797 -#: netbox/dcim/tables/devices.py:1058 netbox/templates/dcim/device.html:132 +#: netbox/dcim/tables/devices.py:1059 netbox/templates/dcim/device.html:132 #: netbox/templates/dcim/virtualchassis.html:27 #: netbox/templates/dcim/virtualchassis.html:67 msgid "Master" @@ -5032,7 +5032,7 @@ msgstr "" msgid "Power Feed" msgstr "" -#: netbox/dcim/forms/filtersets.py:137 netbox/dcim/tables/devices.py:304 +#: netbox/dcim/forms/filtersets.py:137 netbox/dcim/tables/devices.py:305 msgid "Device Status" msgstr "" @@ -5104,8 +5104,8 @@ msgstr "" #: netbox/dcim/forms/filtersets.py:1245 netbox/dcim/forms/filtersets.py:1270 #: netbox/dcim/forms/filtersets.py:1294 netbox/dcim/forms/filtersets.py:1314 -#: netbox/dcim/forms/filtersets.py:1341 netbox/dcim/tables/devices.py:373 -#: netbox/dcim/tables/devices.py:662 +#: netbox/dcim/forms/filtersets.py:1341 netbox/dcim/tables/devices.py:374 +#: netbox/dcim/tables/devices.py:663 #: netbox/templates/circuits/inc/circuit_termination_fields.html:16 #: netbox/templates/dcim/consoleport.html:55 #: netbox/templates/dcim/consoleserverport.html:55 @@ -5120,7 +5120,7 @@ msgstr "" #: netbox/dcim/forms/filtersets.py:1353 netbox/extras/forms/bulk_edit.py:326 #: netbox/extras/forms/bulk_import.py:247 netbox/extras/forms/filtersets.py:472 -#: netbox/extras/forms/model_forms.py:675 netbox/extras/tables/tables.py:579 +#: netbox/extras/forms/model_forms.py:689 netbox/extras/tables/tables.py:579 #: netbox/templates/extras/journalentry.html:30 msgid "Kind" msgstr "" @@ -5152,7 +5152,7 @@ msgid "Transmit power (dBm)" msgstr "" #: netbox/dcim/forms/filtersets.py:1451 netbox/dcim/forms/filtersets.py:1476 -#: netbox/dcim/tables/devices.py:336 netbox/templates/dcim/cable.html:12 +#: netbox/dcim/tables/devices.py:337 netbox/templates/dcim/cable.html:12 #: netbox/templates/dcim/cable_trace.html:46 #: netbox/templates/dcim/frontport.html:77 #: netbox/templates/dcim/htmx/cable_edit.html:50 @@ -5162,7 +5162,7 @@ msgstr "" msgid "Cable" msgstr "" -#: netbox/dcim/forms/filtersets.py:1555 netbox/dcim/tables/devices.py:978 +#: netbox/dcim/forms/filtersets.py:1555 netbox/dcim/tables/devices.py:979 msgid "Discovered" msgstr "" @@ -5180,15 +5180,15 @@ msgid "A virtual chassis member already exists in position {vc_position}." msgstr "" #: netbox/dcim/forms/mixins.py:27 netbox/dcim/forms/mixins.py:75 -#: netbox/ipam/forms/bulk_edit.py:420 netbox/ipam/forms/model_forms.py:610 +#: netbox/ipam/forms/bulk_edit.py:420 netbox/ipam/forms/model_forms.py:618 msgid "Scope type" msgstr "" #: netbox/dcim/forms/mixins.py:30 netbox/dcim/forms/mixins.py:78 #: netbox/ipam/forms/bulk_edit.py:270 netbox/ipam/forms/bulk_edit.py:423 #: netbox/ipam/forms/bulk_edit.py:437 netbox/ipam/forms/filtersets.py:175 -#: netbox/ipam/forms/model_forms.py:231 netbox/ipam/forms/model_forms.py:613 -#: netbox/ipam/forms/model_forms.py:623 netbox/ipam/tables/ip.py:194 +#: netbox/ipam/forms/model_forms.py:231 netbox/ipam/forms/model_forms.py:621 +#: netbox/ipam/forms/model_forms.py:631 netbox/ipam/tables/ip.py:194 #: netbox/ipam/tables/vlans.py:40 netbox/templates/ipam/prefix.html:48 #: netbox/templates/ipam/vlangroup.html:38 #: netbox/templates/virtualization/cluster.html:42 @@ -5328,7 +5328,7 @@ msgid "Front Port" msgstr "" #: netbox/dcim/forms/model_forms.py:1168 netbox/dcim/forms/model_forms.py:1639 -#: netbox/dcim/tables/devices.py:743 +#: netbox/dcim/tables/devices.py:744 #: netbox/templates/circuits/inc/circuit_termination_fields.html:54 #: netbox/templates/dcim/consoleport.html:79 #: netbox/templates/dcim/consoleserverport.html:80 @@ -5341,7 +5341,7 @@ msgid "Rear Port" msgstr "" #: netbox/dcim/forms/model_forms.py:1169 netbox/dcim/forms/model_forms.py:1640 -#: netbox/dcim/tables/connections.py:46 netbox/dcim/tables/devices.py:520 +#: netbox/dcim/tables/connections.py:46 netbox/dcim/tables/devices.py:521 #: netbox/templates/dcim/poweroutlet.html:54 #: netbox/templates/dcim/powerport.html:17 msgid "Power Port" @@ -5410,8 +5410,8 @@ msgid "VM Interface" msgstr "" #: netbox/dcim/forms/model_forms.py:1788 netbox/ipam/forms/filtersets.py:608 -#: netbox/ipam/forms/model_forms.py:326 netbox/ipam/forms/model_forms.py:788 -#: netbox/ipam/forms/model_forms.py:814 netbox/ipam/tables/vlans.py:171 +#: netbox/ipam/forms/model_forms.py:334 netbox/ipam/forms/model_forms.py:796 +#: netbox/ipam/forms/model_forms.py:822 netbox/ipam/tables/vlans.py:171 #: netbox/templates/virtualization/virtualdisk.html:21 #: netbox/templates/virtualization/virtualmachine.html:12 #: netbox/templates/virtualization/vminterface.html:21 @@ -5446,7 +5446,7 @@ msgid "" msgstr "" #: netbox/dcim/forms/object_create.py:110 -#: netbox/dcim/forms/object_create.py:263 netbox/dcim/tables/devices.py:262 +#: netbox/dcim/forms/object_create.py:263 netbox/dcim/tables/devices.py:263 msgid "Rear ports" msgstr "" @@ -5469,7 +5469,7 @@ msgid "" "selected number of rear port positions ({rearport_count})." msgstr "" -#: netbox/dcim/forms/object_create.py:401 netbox/dcim/tables/devices.py:1064 +#: netbox/dcim/forms/object_create.py:401 netbox/dcim/tables/devices.py:1065 #: netbox/ipam/tables/fhrp.py:31 netbox/templates/dcim/virtualchassis.html:53 #: netbox/templates/dcim/virtualchassis_edit.html:47 #: netbox/templates/ipam/fhrpgroup.html:38 @@ -5915,9 +5915,9 @@ msgid "tagged VLANs" msgstr "" #: netbox/dcim/models/device_components.py:561 -#: netbox/dcim/tables/devices.py:601 netbox/ipam/forms/bulk_edit.py:510 +#: netbox/dcim/tables/devices.py:602 netbox/ipam/forms/bulk_edit.py:510 #: netbox/ipam/forms/bulk_import.py:491 netbox/ipam/forms/filtersets.py:565 -#: netbox/ipam/forms/model_forms.py:684 netbox/ipam/tables/vlans.py:106 +#: netbox/ipam/forms/model_forms.py:692 netbox/ipam/tables/vlans.py:106 #: netbox/templates/dcim/interface.html:86 netbox/templates/ipam/vlan.html:77 msgid "Q-in-Q SVLAN" msgstr "" @@ -6149,7 +6149,7 @@ msgid "module bays" msgstr "" #: netbox/dcim/models/device_components.py:1178 -#: netbox/dcim/models/devices.py:1225 +#: netbox/dcim/models/devices.py:1229 msgid "A module bay cannot belong to a module installed within it." msgstr "" @@ -6185,14 +6185,14 @@ msgid "inventory item roles" msgstr "" #: netbox/dcim/models/device_components.py:1308 -#: netbox/dcim/models/devices.py:598 netbox/dcim/models/devices.py:1185 +#: netbox/dcim/models/devices.py:598 netbox/dcim/models/devices.py:1189 #: netbox/dcim/models/racks.py:304 #: netbox/virtualization/models/virtualmachines.py:126 msgid "serial number" msgstr "" #: netbox/dcim/models/device_components.py:1316 -#: netbox/dcim/models/devices.py:606 netbox/dcim/models/devices.py:1192 +#: netbox/dcim/models/devices.py:606 netbox/dcim/models/devices.py:1196 #: netbox/dcim/models/racks.py:311 msgid "asset tag" msgstr "" @@ -6369,7 +6369,7 @@ msgstr "" msgid "Chassis serial number, assigned by the manufacturer" msgstr "" -#: netbox/dcim/models/devices.py:607 netbox/dcim/models/devices.py:1193 +#: netbox/dcim/models/devices.py:607 netbox/dcim/models/devices.py:1197 msgid "A unique tag used to identify this device" msgstr "" @@ -6381,12 +6381,12 @@ msgstr "" msgid "rack face" msgstr "" -#: netbox/dcim/models/devices.py:663 netbox/dcim/models/devices.py:1421 +#: netbox/dcim/models/devices.py:663 netbox/dcim/models/devices.py:1425 #: netbox/virtualization/models/virtualmachines.py:95 msgid "primary IPv4" msgstr "" -#: netbox/dcim/models/devices.py:671 netbox/dcim/models/devices.py:1429 +#: netbox/dcim/models/devices.py:671 netbox/dcim/models/devices.py:1433 #: netbox/virtualization/models/virtualmachines.py:103 msgid "primary IPv6" msgstr "" @@ -6436,181 +6436,181 @@ msgstr "" msgid "devices" msgstr "" -#: netbox/dcim/models/devices.py:825 +#: netbox/dcim/models/devices.py:821 #, python-brace-format msgid "Rack {rack} does not belong to site {site}." msgstr "" -#: netbox/dcim/models/devices.py:830 +#: netbox/dcim/models/devices.py:826 #, python-brace-format msgid "Location {location} does not belong to site {site}." msgstr "" -#: netbox/dcim/models/devices.py:836 +#: netbox/dcim/models/devices.py:832 #, python-brace-format msgid "Rack {rack} does not belong to location {location}." msgstr "" -#: netbox/dcim/models/devices.py:843 +#: netbox/dcim/models/devices.py:839 msgid "Cannot select a rack face without assigning a rack." msgstr "" -#: netbox/dcim/models/devices.py:847 +#: netbox/dcim/models/devices.py:843 msgid "Cannot select a rack position without assigning a rack." msgstr "" -#: netbox/dcim/models/devices.py:853 +#: netbox/dcim/models/devices.py:849 msgid "Position must be in increments of 0.5 rack units." msgstr "" -#: netbox/dcim/models/devices.py:857 +#: netbox/dcim/models/devices.py:853 msgid "Must specify rack face when defining rack position." msgstr "" -#: netbox/dcim/models/devices.py:865 +#: netbox/dcim/models/devices.py:861 #, python-brace-format msgid "A 0U device type ({device_type}) cannot be assigned to a rack position." msgstr "" -#: netbox/dcim/models/devices.py:876 +#: netbox/dcim/models/devices.py:872 msgid "" "Child device types cannot be assigned to a rack face. This is an attribute " "of the parent device." msgstr "" -#: netbox/dcim/models/devices.py:883 +#: netbox/dcim/models/devices.py:879 msgid "" "Child device types cannot be assigned to a rack position. This is an " "attribute of the parent device." msgstr "" -#: netbox/dcim/models/devices.py:897 +#: netbox/dcim/models/devices.py:893 #, python-brace-format msgid "" "U{position} is already occupied or does not have sufficient space to " "accommodate this device type: {device_type} ({u_height}U)" msgstr "" -#: netbox/dcim/models/devices.py:912 +#: netbox/dcim/models/devices.py:908 #, python-brace-format msgid "{ip} is not an IPv4 address." msgstr "" -#: netbox/dcim/models/devices.py:924 netbox/dcim/models/devices.py:942 +#: netbox/dcim/models/devices.py:920 netbox/dcim/models/devices.py:938 #, python-brace-format msgid "The specified IP address ({ip}) is not assigned to this device." msgstr "" -#: netbox/dcim/models/devices.py:930 +#: netbox/dcim/models/devices.py:926 #, python-brace-format msgid "{ip} is not an IPv6 address." msgstr "" -#: netbox/dcim/models/devices.py:960 +#: netbox/dcim/models/devices.py:956 #, python-brace-format msgid "" "The assigned platform is limited to {platform_manufacturer} device types, " "but this device's type belongs to {devicetype_manufacturer}." msgstr "" -#: netbox/dcim/models/devices.py:971 +#: netbox/dcim/models/devices.py:967 #, python-brace-format msgid "The assigned cluster belongs to a different site ({site})" msgstr "" -#: netbox/dcim/models/devices.py:978 +#: netbox/dcim/models/devices.py:974 #, python-brace-format msgid "The assigned cluster belongs to a different location ({location})" msgstr "" -#: netbox/dcim/models/devices.py:986 +#: netbox/dcim/models/devices.py:982 msgid "A device assigned to a virtual chassis must have its position defined." msgstr "" -#: netbox/dcim/models/devices.py:992 +#: netbox/dcim/models/devices.py:988 #, python-brace-format msgid "" "Device cannot be removed from virtual chassis {virtual_chassis} because it " "is currently designated as its master." msgstr "" -#: netbox/dcim/models/devices.py:1200 +#: netbox/dcim/models/devices.py:1204 msgid "module" msgstr "" -#: netbox/dcim/models/devices.py:1201 +#: netbox/dcim/models/devices.py:1205 msgid "modules" msgstr "" -#: netbox/dcim/models/devices.py:1214 +#: netbox/dcim/models/devices.py:1218 #, python-brace-format msgid "" "Module must be installed within a module bay belonging to the assigned " "device ({device})." msgstr "" -#: netbox/dcim/models/devices.py:1342 +#: netbox/dcim/models/devices.py:1346 msgid "domain" msgstr "" -#: netbox/dcim/models/devices.py:1355 netbox/dcim/models/devices.py:1356 +#: netbox/dcim/models/devices.py:1359 netbox/dcim/models/devices.py:1360 msgid "virtual chassis" msgstr "" -#: netbox/dcim/models/devices.py:1368 +#: netbox/dcim/models/devices.py:1372 #, python-brace-format msgid "The selected master ({master}) is not assigned to this virtual chassis." msgstr "" -#: netbox/dcim/models/devices.py:1384 +#: netbox/dcim/models/devices.py:1388 #, python-brace-format msgid "" "Unable to delete virtual chassis {self}. There are member interfaces which " "form a cross-chassis LAG interfaces." msgstr "" -#: netbox/dcim/models/devices.py:1410 netbox/vpn/models/l2vpn.py:37 +#: netbox/dcim/models/devices.py:1414 netbox/vpn/models/l2vpn.py:37 msgid "identifier" msgstr "" -#: netbox/dcim/models/devices.py:1411 +#: netbox/dcim/models/devices.py:1415 msgid "Numeric identifier unique to the parent device" msgstr "" -#: netbox/dcim/models/devices.py:1439 netbox/extras/models/customfields.py:225 +#: netbox/dcim/models/devices.py:1443 netbox/extras/models/customfields.py:225 #: netbox/extras/models/models.py:107 netbox/extras/models/models.py:694 #: netbox/netbox/models/__init__.py:120 msgid "comments" msgstr "" -#: netbox/dcim/models/devices.py:1455 +#: netbox/dcim/models/devices.py:1459 msgid "virtual device context" msgstr "" -#: netbox/dcim/models/devices.py:1456 +#: netbox/dcim/models/devices.py:1460 msgid "virtual device contexts" msgstr "" -#: netbox/dcim/models/devices.py:1485 +#: netbox/dcim/models/devices.py:1489 #, python-brace-format msgid "{ip} is not an IPv{family} address." msgstr "" -#: netbox/dcim/models/devices.py:1491 +#: netbox/dcim/models/devices.py:1495 msgid "Primary IP address must belong to an interface on the assigned device." msgstr "" -#: netbox/dcim/models/devices.py:1523 +#: netbox/dcim/models/devices.py:1527 msgid "MAC addresses" msgstr "" -#: netbox/dcim/models/devices.py:1552 +#: netbox/dcim/models/devices.py:1556 msgid "" "Cannot unassign MAC Address while it is designated as the primary MAC for an " "object" msgstr "" -#: netbox/dcim/models/devices.py:1556 +#: netbox/dcim/models/devices.py:1560 msgid "" "Cannot reassign MAC Address while it is designated as the primary MAC for an " "object" @@ -7000,8 +7000,8 @@ msgstr "" msgid "VMs" msgstr "" -#: netbox/dcim/tables/devices.py:111 netbox/dcim/tables/devices.py:226 -#: netbox/extras/forms/model_forms.py:630 netbox/templates/dcim/device.html:112 +#: netbox/dcim/tables/devices.py:111 netbox/dcim/tables/devices.py:227 +#: netbox/extras/forms/model_forms.py:644 netbox/templates/dcim/device.html:112 #: netbox/templates/dcim/devicerole.html:44 #: netbox/templates/dcim/platform.html:41 #: netbox/templates/extras/configtemplate.html:10 @@ -7012,59 +7012,59 @@ msgstr "" msgid "Config Template" msgstr "" -#: netbox/dcim/tables/devices.py:197 netbox/dcim/tables/devices.py:1099 -#: netbox/ipam/forms/bulk_import.py:562 netbox/ipam/forms/model_forms.py:308 -#: netbox/ipam/forms/model_forms.py:321 netbox/ipam/tables/ip.py:308 +#: netbox/dcim/tables/devices.py:198 netbox/dcim/tables/devices.py:1100 +#: netbox/ipam/forms/bulk_import.py:562 netbox/ipam/forms/model_forms.py:316 +#: netbox/ipam/forms/model_forms.py:329 netbox/ipam/tables/ip.py:308 #: netbox/ipam/tables/ip.py:375 netbox/ipam/tables/ip.py:398 #: netbox/templates/ipam/ipaddress.html:11 #: netbox/virtualization/tables/virtualmachines.py:65 msgid "IP Address" msgstr "" -#: netbox/dcim/tables/devices.py:201 netbox/dcim/tables/devices.py:1103 +#: netbox/dcim/tables/devices.py:202 netbox/dcim/tables/devices.py:1104 #: netbox/virtualization/tables/virtualmachines.py:56 msgid "IPv4 Address" msgstr "" -#: netbox/dcim/tables/devices.py:205 netbox/dcim/tables/devices.py:1107 +#: netbox/dcim/tables/devices.py:206 netbox/dcim/tables/devices.py:1108 #: netbox/virtualization/tables/virtualmachines.py:60 msgid "IPv6 Address" msgstr "" -#: netbox/dcim/tables/devices.py:220 +#: netbox/dcim/tables/devices.py:221 msgid "VC Position" msgstr "" -#: netbox/dcim/tables/devices.py:223 +#: netbox/dcim/tables/devices.py:224 msgid "VC Priority" msgstr "" -#: netbox/dcim/tables/devices.py:230 netbox/templates/dcim/device_edit.html:38 +#: netbox/dcim/tables/devices.py:231 netbox/templates/dcim/device_edit.html:38 #: netbox/templates/dcim/devicebay_populate.html:16 msgid "Parent Device" msgstr "" -#: netbox/dcim/tables/devices.py:235 +#: netbox/dcim/tables/devices.py:236 msgid "Position (Device Bay)" msgstr "" -#: netbox/dcim/tables/devices.py:244 +#: netbox/dcim/tables/devices.py:245 msgid "Console ports" msgstr "" -#: netbox/dcim/tables/devices.py:247 +#: netbox/dcim/tables/devices.py:248 msgid "Console server ports" msgstr "" -#: netbox/dcim/tables/devices.py:250 +#: netbox/dcim/tables/devices.py:251 msgid "Power ports" msgstr "" -#: netbox/dcim/tables/devices.py:253 +#: netbox/dcim/tables/devices.py:254 msgid "Power outlets" msgstr "" -#: netbox/dcim/tables/devices.py:256 netbox/dcim/tables/devices.py:1112 +#: netbox/dcim/tables/devices.py:257 netbox/dcim/tables/devices.py:1113 #: netbox/dcim/tables/devicetypes.py:133 netbox/dcim/views.py:1144 #: netbox/dcim/views.py:1388 netbox/dcim/views.py:2139 #: netbox/netbox/navigation/menu.py:94 netbox/netbox/navigation/menu.py:258 @@ -7082,28 +7082,28 @@ msgstr "" msgid "Interfaces" msgstr "" -#: netbox/dcim/tables/devices.py:259 +#: netbox/dcim/tables/devices.py:260 msgid "Front ports" msgstr "" -#: netbox/dcim/tables/devices.py:265 +#: netbox/dcim/tables/devices.py:266 msgid "Device bays" msgstr "" -#: netbox/dcim/tables/devices.py:268 +#: netbox/dcim/tables/devices.py:269 msgid "Module bays" msgstr "" -#: netbox/dcim/tables/devices.py:271 +#: netbox/dcim/tables/devices.py:272 msgid "Inventory items" msgstr "" -#: netbox/dcim/tables/devices.py:314 netbox/dcim/tables/modules.py:57 +#: netbox/dcim/tables/devices.py:315 netbox/dcim/tables/modules.py:57 #: netbox/templates/dcim/modulebay.html:17 msgid "Module Bay" msgstr "" -#: netbox/dcim/tables/devices.py:327 netbox/dcim/tables/devicetypes.py:52 +#: netbox/dcim/tables/devices.py:328 netbox/dcim/tables/devicetypes.py:52 #: netbox/dcim/tables/devicetypes.py:148 netbox/dcim/views.py:1219 #: netbox/dcim/views.py:2237 netbox/netbox/navigation/menu.py:103 #: netbox/templates/dcim/device/base.html:52 @@ -7114,27 +7114,27 @@ msgstr "" msgid "Inventory Items" msgstr "" -#: netbox/dcim/tables/devices.py:342 +#: netbox/dcim/tables/devices.py:343 msgid "Cable Color" msgstr "" -#: netbox/dcim/tables/devices.py:348 +#: netbox/dcim/tables/devices.py:349 msgid "Link Peers" msgstr "" -#: netbox/dcim/tables/devices.py:351 +#: netbox/dcim/tables/devices.py:352 msgid "Mark Connected" msgstr "" -#: netbox/dcim/tables/devices.py:470 +#: netbox/dcim/tables/devices.py:471 msgid "Maximum draw (W)" msgstr "" -#: netbox/dcim/tables/devices.py:473 +#: netbox/dcim/tables/devices.py:474 msgid "Allocated draw (W)" msgstr "" -#: netbox/dcim/tables/devices.py:571 netbox/ipam/forms/model_forms.py:776 +#: netbox/dcim/tables/devices.py:572 netbox/ipam/forms/model_forms.py:784 #: netbox/ipam/tables/fhrp.py:28 netbox/ipam/views.py:633 #: netbox/ipam/views.py:738 netbox/netbox/navigation/menu.py:164 #: netbox/netbox/navigation/menu.py:166 @@ -7146,12 +7146,12 @@ msgstr "" msgid "IP Addresses" msgstr "" -#: netbox/dcim/tables/devices.py:577 netbox/netbox/navigation/menu.py:210 +#: netbox/dcim/tables/devices.py:578 netbox/netbox/navigation/menu.py:210 #: netbox/templates/ipam/inc/panels/fhrp_groups.html:6 msgid "FHRP Groups" msgstr "" -#: netbox/dcim/tables/devices.py:589 netbox/templates/dcim/interface.html:95 +#: netbox/dcim/tables/devices.py:590 netbox/templates/dcim/interface.html:95 #: netbox/templates/virtualization/vminterface.html:59 #: netbox/templates/vpn/tunnel.html:18 #: netbox/templates/vpn/tunneltermination.html:13 @@ -7162,41 +7162,41 @@ msgstr "" msgid "Tunnel" msgstr "" -#: netbox/dcim/tables/devices.py:625 netbox/dcim/tables/devicetypes.py:234 +#: netbox/dcim/tables/devices.py:626 netbox/dcim/tables/devicetypes.py:234 #: netbox/templates/dcim/interface.html:65 msgid "Management Only" msgstr "" -#: netbox/dcim/tables/devices.py:644 +#: netbox/dcim/tables/devices.py:645 msgid "VDCs" msgstr "" -#: netbox/dcim/tables/devices.py:651 netbox/templates/dcim/interface.html:163 +#: netbox/dcim/tables/devices.py:652 netbox/templates/dcim/interface.html:163 msgid "Virtual Circuit" msgstr "" -#: netbox/dcim/tables/devices.py:903 netbox/templates/dcim/modulebay.html:53 +#: netbox/dcim/tables/devices.py:904 netbox/templates/dcim/modulebay.html:53 msgid "Installed Module" msgstr "" -#: netbox/dcim/tables/devices.py:906 +#: netbox/dcim/tables/devices.py:907 msgid "Module Serial" msgstr "" -#: netbox/dcim/tables/devices.py:910 +#: netbox/dcim/tables/devices.py:911 msgid "Module Asset Tag" msgstr "" -#: netbox/dcim/tables/devices.py:919 +#: netbox/dcim/tables/devices.py:920 msgid "Module Status" msgstr "" -#: netbox/dcim/tables/devices.py:973 netbox/dcim/tables/devicetypes.py:319 +#: netbox/dcim/tables/devices.py:974 netbox/dcim/tables/devicetypes.py:319 #: netbox/templates/dcim/inventoryitem.html:44 msgid "Component" msgstr "" -#: netbox/dcim/tables/devices.py:1031 +#: netbox/dcim/tables/devices.py:1032 msgid "Items" msgstr "" @@ -7215,7 +7215,7 @@ msgid "Module Types" msgstr "" #: netbox/dcim/tables/devicetypes.py:57 netbox/extras/forms/filtersets.py:378 -#: netbox/extras/forms/model_forms.py:537 netbox/extras/tables/tables.py:540 +#: netbox/extras/forms/model_forms.py:551 netbox/extras/tables/tables.py:540 #: netbox/netbox/navigation/menu.py:78 msgid "Platforms" msgstr "" @@ -7367,7 +7367,7 @@ msgid "Space" msgstr "" #: netbox/dcim/tables/sites.py:30 netbox/dcim/tables/sites.py:57 -#: netbox/extras/forms/filtersets.py:358 netbox/extras/forms/model_forms.py:517 +#: netbox/extras/forms/filtersets.py:358 netbox/extras/forms/model_forms.py:531 #: netbox/ipam/forms/bulk_edit.py:134 netbox/ipam/forms/model_forms.py:159 #: netbox/ipam/tables/asn.py:66 netbox/netbox/navigation/menu.py:15 #: netbox/netbox/navigation/menu.py:17 @@ -7392,7 +7392,7 @@ msgstr "" msgid "Non-Racked Devices" msgstr "" -#: netbox/dcim/views.py:2250 netbox/extras/forms/model_forms.py:577 +#: netbox/dcim/views.py:2250 netbox/extras/forms/model_forms.py:591 #: netbox/templates/extras/configcontext.html:10 #: netbox/virtualization/forms/model_forms.py:232 #: netbox/virtualization/views.py:422 @@ -7669,13 +7669,13 @@ msgstr "" msgid "White" msgstr "" -#: netbox/extras/choices.py:266 netbox/extras/forms/model_forms.py:353 -#: netbox/extras/forms/model_forms.py:430 +#: netbox/extras/choices.py:266 netbox/extras/forms/model_forms.py:367 +#: netbox/extras/forms/model_forms.py:444 #: netbox/templates/extras/webhook.html:10 msgid "Webhook" msgstr "" -#: netbox/extras/choices.py:267 netbox/extras/forms/model_forms.py:418 +#: netbox/extras/choices.py:267 netbox/extras/forms/model_forms.py:432 #: netbox/templates/extras/script/base.html:29 msgid "Script" msgstr "" @@ -7846,7 +7846,7 @@ msgstr "" msgid "Tenant group (slug)" msgstr "" -#: netbox/extras/filtersets.py:623 netbox/extras/forms/model_forms.py:495 +#: netbox/extras/filtersets.py:623 netbox/extras/forms/model_forms.py:509 #: netbox/templates/extras/tag.html:11 msgid "Tag" msgstr "" @@ -7958,7 +7958,7 @@ msgid "CA file path" msgstr "" #: netbox/extras/forms/bulk_edit.py:253 netbox/extras/forms/bulk_import.py:192 -#: netbox/extras/forms/model_forms.py:377 +#: netbox/extras/forms/model_forms.py:391 msgid "Event types" msgstr "" @@ -7971,11 +7971,11 @@ msgstr "" #: netbox/extras/forms/bulk_import.py:162 #: netbox/extras/forms/bulk_import.py:186 netbox/extras/forms/filtersets.py:140 #: netbox/extras/forms/filtersets.py:230 netbox/extras/forms/model_forms.py:47 -#: netbox/extras/forms/model_forms.py:205 -#: netbox/extras/forms/model_forms.py:237 -#: netbox/extras/forms/model_forms.py:278 -#: netbox/extras/forms/model_forms.py:372 -#: netbox/extras/forms/model_forms.py:489 netbox/users/forms/model_forms.py:276 +#: netbox/extras/forms/model_forms.py:219 +#: netbox/extras/forms/model_forms.py:251 +#: netbox/extras/forms/model_forms.py:292 +#: netbox/extras/forms/model_forms.py:386 +#: netbox/extras/forms/model_forms.py:503 netbox/users/forms/model_forms.py:276 msgid "Object types" msgstr "" @@ -7992,8 +7992,8 @@ msgid "Field data type (e.g. text, integer, etc.)" msgstr "" #: netbox/extras/forms/bulk_import.py:47 netbox/extras/forms/filtersets.py:213 -#: netbox/extras/forms/filtersets.py:287 netbox/extras/forms/model_forms.py:304 -#: netbox/extras/forms/model_forms.py:341 netbox/tenancy/forms/filtersets.py:92 +#: netbox/extras/forms/filtersets.py:287 netbox/extras/forms/model_forms.py:318 +#: netbox/extras/forms/model_forms.py:355 netbox/tenancy/forms/filtersets.py:92 msgid "Object type" msgstr "" @@ -8067,7 +8067,7 @@ msgid "The classification of entry" msgstr "" #: netbox/extras/forms/bulk_import.py:261 -#: netbox/extras/forms/model_forms.py:320 netbox/netbox/navigation/menu.py:411 +#: netbox/extras/forms/model_forms.py:334 netbox/netbox/navigation/menu.py:411 #: netbox/templates/extras/notificationgroup.html:41 #: netbox/templates/users/group.html:29 netbox/users/forms/model_forms.py:236 #: netbox/users/forms/model_forms.py:248 netbox/users/forms/model_forms.py:300 @@ -8080,7 +8080,7 @@ msgid "User names separated by commas, encased with double quotes" msgstr "" #: netbox/extras/forms/bulk_import.py:268 -#: netbox/extras/forms/model_forms.py:315 netbox/netbox/navigation/menu.py:294 +#: netbox/extras/forms/model_forms.py:329 netbox/netbox/navigation/menu.py:294 #: netbox/netbox/navigation/menu.py:431 #: netbox/templates/extras/notificationgroup.html:31 #: netbox/users/forms/model_forms.py:181 netbox/users/forms/model_forms.py:193 @@ -8108,7 +8108,7 @@ msgid "Choices" msgstr "" #: netbox/extras/forms/filtersets.py:168 netbox/extras/forms/filtersets.py:326 -#: netbox/extras/forms/filtersets.py:416 netbox/extras/forms/model_forms.py:572 +#: netbox/extras/forms/filtersets.py:416 netbox/extras/forms/model_forms.py:586 #: netbox/templates/core/job.html:96 netbox/templates/extras/eventrule.html:84 msgid "Data" msgstr "" @@ -8143,37 +8143,37 @@ msgstr "" msgid "Allowed object type" msgstr "" -#: netbox/extras/forms/filtersets.py:348 netbox/extras/forms/model_forms.py:507 +#: netbox/extras/forms/filtersets.py:348 netbox/extras/forms/model_forms.py:521 #: netbox/netbox/navigation/menu.py:18 msgid "Regions" msgstr "" -#: netbox/extras/forms/filtersets.py:353 netbox/extras/forms/model_forms.py:512 +#: netbox/extras/forms/filtersets.py:353 netbox/extras/forms/model_forms.py:526 msgid "Site groups" msgstr "" -#: netbox/extras/forms/filtersets.py:363 netbox/extras/forms/model_forms.py:522 +#: netbox/extras/forms/filtersets.py:363 netbox/extras/forms/model_forms.py:536 #: netbox/netbox/navigation/menu.py:20 netbox/templates/dcim/site.html:127 msgid "Locations" msgstr "" -#: netbox/extras/forms/filtersets.py:368 netbox/extras/forms/model_forms.py:527 +#: netbox/extras/forms/filtersets.py:368 netbox/extras/forms/model_forms.py:541 msgid "Device types" msgstr "" -#: netbox/extras/forms/filtersets.py:373 netbox/extras/forms/model_forms.py:532 +#: netbox/extras/forms/filtersets.py:373 netbox/extras/forms/model_forms.py:546 msgid "Roles" msgstr "" -#: netbox/extras/forms/filtersets.py:383 netbox/extras/forms/model_forms.py:542 +#: netbox/extras/forms/filtersets.py:383 netbox/extras/forms/model_forms.py:556 msgid "Cluster types" msgstr "" -#: netbox/extras/forms/filtersets.py:388 netbox/extras/forms/model_forms.py:547 +#: netbox/extras/forms/filtersets.py:388 netbox/extras/forms/model_forms.py:561 msgid "Cluster groups" msgstr "" -#: netbox/extras/forms/filtersets.py:393 netbox/extras/forms/model_forms.py:552 +#: netbox/extras/forms/filtersets.py:393 netbox/extras/forms/model_forms.py:566 #: netbox/netbox/navigation/menu.py:263 netbox/netbox/navigation/menu.py:265 #: netbox/templates/virtualization/clustertype.html:30 #: netbox/virtualization/tables/clusters.py:23 @@ -8181,7 +8181,7 @@ msgstr "" msgid "Clusters" msgstr "" -#: netbox/extras/forms/filtersets.py:398 netbox/extras/forms/model_forms.py:557 +#: netbox/extras/forms/filtersets.py:398 netbox/extras/forms/model_forms.py:571 msgid "Tenant groups" msgstr "" @@ -8227,118 +8227,118 @@ msgstr "" msgid "Related Object" msgstr "" -#: netbox/extras/forms/model_forms.py:169 +#: netbox/extras/forms/model_forms.py:170 msgid "" "Enter one choice per line. An optional label may be specified for each " "choice by appending it with a colon. Example:" msgstr "" -#: netbox/extras/forms/model_forms.py:212 +#: netbox/extras/forms/model_forms.py:226 #: netbox/templates/extras/customlink.html:10 msgid "Custom Link" msgstr "" -#: netbox/extras/forms/model_forms.py:214 +#: netbox/extras/forms/model_forms.py:228 msgid "Templates" msgstr "" -#: netbox/extras/forms/model_forms.py:226 +#: netbox/extras/forms/model_forms.py:240 #, python-brace-format msgid "" "Jinja2 template code for the link text. Reference the object as {example}. " "Links which render as empty text will not be displayed." msgstr "" -#: netbox/extras/forms/model_forms.py:230 +#: netbox/extras/forms/model_forms.py:244 #, python-brace-format msgid "" "Jinja2 template code for the link URL. Reference the object as {example}." msgstr "" -#: netbox/extras/forms/model_forms.py:241 -#: netbox/extras/forms/model_forms.py:624 +#: netbox/extras/forms/model_forms.py:255 +#: netbox/extras/forms/model_forms.py:638 msgid "Template code" msgstr "" -#: netbox/extras/forms/model_forms.py:247 +#: netbox/extras/forms/model_forms.py:261 #: netbox/templates/extras/exporttemplate.html:12 msgid "Export Template" msgstr "" -#: netbox/extras/forms/model_forms.py:249 +#: netbox/extras/forms/model_forms.py:263 msgid "Rendering" msgstr "" -#: netbox/extras/forms/model_forms.py:263 -#: netbox/extras/forms/model_forms.py:649 +#: netbox/extras/forms/model_forms.py:277 +#: netbox/extras/forms/model_forms.py:663 msgid "Template content is populated from the remote source selected below." msgstr "" -#: netbox/extras/forms/model_forms.py:270 -#: netbox/extras/forms/model_forms.py:656 +#: netbox/extras/forms/model_forms.py:284 +#: netbox/extras/forms/model_forms.py:670 msgid "Must specify either local content or a data file" msgstr "" -#: netbox/extras/forms/model_forms.py:284 netbox/netbox/forms/mixins.py:70 +#: netbox/extras/forms/model_forms.py:298 netbox/netbox/forms/mixins.py:70 #: netbox/templates/extras/savedfilter.html:10 msgid "Saved Filter" msgstr "" -#: netbox/extras/forms/model_forms.py:334 +#: netbox/extras/forms/model_forms.py:348 msgid "A notification group specify at least one user or group." msgstr "" -#: netbox/extras/forms/model_forms.py:356 +#: netbox/extras/forms/model_forms.py:370 #: netbox/templates/extras/webhook.html:23 msgid "HTTP Request" msgstr "" -#: netbox/extras/forms/model_forms.py:358 +#: netbox/extras/forms/model_forms.py:372 #: netbox/templates/extras/webhook.html:44 msgid "SSL" msgstr "" -#: netbox/extras/forms/model_forms.py:380 +#: netbox/extras/forms/model_forms.py:394 msgid "Action choice" msgstr "" -#: netbox/extras/forms/model_forms.py:385 +#: netbox/extras/forms/model_forms.py:399 msgid "Enter conditions in JSON format." msgstr "" -#: netbox/extras/forms/model_forms.py:389 +#: netbox/extras/forms/model_forms.py:403 msgid "" "Enter parameters to pass to the action in JSON format." msgstr "" -#: netbox/extras/forms/model_forms.py:394 +#: netbox/extras/forms/model_forms.py:408 #: netbox/templates/extras/eventrule.html:10 msgid "Event Rule" msgstr "" -#: netbox/extras/forms/model_forms.py:395 +#: netbox/extras/forms/model_forms.py:409 msgid "Triggers" msgstr "" -#: netbox/extras/forms/model_forms.py:442 +#: netbox/extras/forms/model_forms.py:456 msgid "Notification group" msgstr "" -#: netbox/extras/forms/model_forms.py:562 netbox/netbox/navigation/menu.py:26 +#: netbox/extras/forms/model_forms.py:576 netbox/netbox/navigation/menu.py:26 #: netbox/tenancy/tables/tenants.py:22 msgid "Tenants" msgstr "" -#: netbox/extras/forms/model_forms.py:606 +#: netbox/extras/forms/model_forms.py:620 msgid "Data is populated from the remote source selected below." msgstr "" -#: netbox/extras/forms/model_forms.py:612 +#: netbox/extras/forms/model_forms.py:626 msgid "Must specify either local data or a data file" msgstr "" -#: netbox/extras/forms/model_forms.py:631 +#: netbox/extras/forms/model_forms.py:645 #: netbox/templates/core/datafile.html:55 msgid "Content" msgstr "" @@ -9371,7 +9371,7 @@ msgstr "" msgid "Error deleting widget: " msgstr "" -#: netbox/extras/views.py:1300 +#: netbox/extras/views.py:1308 msgid "Unable to run script: RQ worker process not running." msgstr "" @@ -9434,8 +9434,8 @@ msgstr "" msgid "Plaintext" msgstr "" -#: netbox/ipam/choices.py:166 netbox/ipam/forms/model_forms.py:792 -#: netbox/ipam/forms/model_forms.py:820 netbox/templates/ipam/service.html:21 +#: netbox/ipam/choices.py:166 netbox/ipam/forms/model_forms.py:800 +#: netbox/ipam/forms/model_forms.py:828 netbox/templates/ipam/service.html:21 msgid "Service" msgstr "" @@ -9539,7 +9539,7 @@ msgid "VLAN number (1-4094)" msgstr "" #: netbox/ipam/filtersets.py:440 netbox/ipam/filtersets.py:444 -#: netbox/ipam/filtersets.py:536 netbox/ipam/forms/model_forms.py:498 +#: netbox/ipam/filtersets.py:536 netbox/ipam/forms/model_forms.py:506 #: netbox/templates/tenancy/contact.html:53 #: netbox/tenancy/forms/bulk_edit.py:113 msgid "Address" @@ -9661,8 +9661,8 @@ msgstr "" msgid "Date added" msgstr "" -#: netbox/ipam/forms/bulk_edit.py:213 netbox/ipam/forms/model_forms.py:621 -#: netbox/ipam/forms/model_forms.py:668 netbox/ipam/tables/ip.py:201 +#: netbox/ipam/forms/bulk_edit.py:213 netbox/ipam/forms/model_forms.py:629 +#: netbox/ipam/forms/model_forms.py:676 netbox/ipam/tables/ip.py:201 #: netbox/templates/ipam/vlan_edit.html:45 #: netbox/templates/ipam/vlangroup.html:27 msgid "VLAN Group" @@ -9738,7 +9738,7 @@ msgid "Authentication key" msgstr "" #: netbox/ipam/forms/bulk_edit.py:405 netbox/ipam/forms/filtersets.py:386 -#: netbox/ipam/forms/model_forms.py:509 netbox/netbox/navigation/menu.py:407 +#: netbox/ipam/forms/model_forms.py:517 netbox/netbox/navigation/menu.py:407 #: netbox/templates/ipam/fhrpgroup.html:49 #: netbox/templates/wireless/inc/authentication_attrs.html:5 #: netbox/wireless/forms/bulk_edit.py:94 netbox/wireless/forms/bulk_edit.py:152 @@ -9768,14 +9768,14 @@ msgid "Site & Group" msgstr "" #: netbox/ipam/forms/bulk_edit.py:546 netbox/ipam/forms/bulk_import.py:515 -#: netbox/ipam/forms/model_forms.py:716 netbox/ipam/tables/vlans.py:256 +#: netbox/ipam/forms/model_forms.py:724 netbox/ipam/tables/vlans.py:256 #: netbox/templates/ipam/vlantranslationrule.html:14 #: netbox/vpn/forms/model_forms.py:322 netbox/vpn/forms/model_forms.py:359 msgid "Policy" msgstr "" -#: netbox/ipam/forms/bulk_edit.py:567 netbox/ipam/forms/model_forms.py:734 -#: netbox/ipam/forms/model_forms.py:766 netbox/ipam/tables/services.py:19 +#: netbox/ipam/forms/bulk_edit.py:567 netbox/ipam/forms/model_forms.py:742 +#: netbox/ipam/forms/model_forms.py:774 netbox/ipam/tables/services.py:19 #: netbox/ipam/tables/services.py:49 netbox/templates/ipam/service.html:36 #: netbox/templates/ipam/servicetemplate.html:23 msgid "Ports" @@ -9980,99 +9980,99 @@ msgstr "" msgid "ASN Range" msgstr "" -#: netbox/ipam/forms/model_forms.py:261 netbox/templates/ipam/iprange.html:10 +#: netbox/ipam/forms/model_forms.py:269 netbox/templates/ipam/iprange.html:10 msgid "IP Range" msgstr "" -#: netbox/ipam/forms/model_forms.py:297 netbox/ipam/forms/model_forms.py:327 -#: netbox/ipam/forms/model_forms.py:508 netbox/templates/ipam/fhrpgroup.html:19 +#: netbox/ipam/forms/model_forms.py:305 netbox/ipam/forms/model_forms.py:335 +#: netbox/ipam/forms/model_forms.py:516 netbox/templates/ipam/fhrpgroup.html:19 msgid "FHRP Group" msgstr "" -#: netbox/ipam/forms/model_forms.py:312 +#: netbox/ipam/forms/model_forms.py:320 msgid "Make this the primary IP for the device/VM" msgstr "" -#: netbox/ipam/forms/model_forms.py:316 +#: netbox/ipam/forms/model_forms.py:324 msgid "Make this the out-of-band IP for the device" msgstr "" -#: netbox/ipam/forms/model_forms.py:331 +#: netbox/ipam/forms/model_forms.py:339 msgid "NAT IP (Inside)" msgstr "" -#: netbox/ipam/forms/model_forms.py:393 +#: netbox/ipam/forms/model_forms.py:401 msgid "An IP address can only be assigned to a single object." msgstr "" -#: netbox/ipam/forms/model_forms.py:400 +#: netbox/ipam/forms/model_forms.py:408 msgid "Cannot reassign primary IP address for the parent device/VM" msgstr "" -#: netbox/ipam/forms/model_forms.py:404 +#: netbox/ipam/forms/model_forms.py:412 msgid "Cannot reassign out-of-Band IP address for the parent device" msgstr "" -#: netbox/ipam/forms/model_forms.py:414 +#: netbox/ipam/forms/model_forms.py:422 msgid "" "Only IP addresses assigned to an interface can be designated as primary IPs." msgstr "" -#: netbox/ipam/forms/model_forms.py:422 +#: netbox/ipam/forms/model_forms.py:430 msgid "" "Only IP addresses assigned to a device interface can be designated as the " "out-of-band IP for a device." msgstr "" -#: netbox/ipam/forms/model_forms.py:510 +#: netbox/ipam/forms/model_forms.py:518 msgid "Virtual IP Address" msgstr "" -#: netbox/ipam/forms/model_forms.py:595 +#: netbox/ipam/forms/model_forms.py:603 msgid "Assignment already exists" msgstr "" -#: netbox/ipam/forms/model_forms.py:604 netbox/templates/ipam/vlangroup.html:42 +#: netbox/ipam/forms/model_forms.py:612 netbox/templates/ipam/vlangroup.html:42 msgid "VLAN IDs" msgstr "" -#: netbox/ipam/forms/model_forms.py:622 +#: netbox/ipam/forms/model_forms.py:630 msgid "Child VLANs" msgstr "" -#: netbox/ipam/forms/model_forms.py:722 +#: netbox/ipam/forms/model_forms.py:730 #: netbox/templates/ipam/vlantranslationrule.html:11 msgid "VLAN Translation Rule" msgstr "" -#: netbox/ipam/forms/model_forms.py:739 netbox/ipam/forms/model_forms.py:771 +#: netbox/ipam/forms/model_forms.py:747 netbox/ipam/forms/model_forms.py:779 msgid "" "Comma-separated list of one or more port numbers. A range may be specified " "using a hyphen." msgstr "" -#: netbox/ipam/forms/model_forms.py:744 +#: netbox/ipam/forms/model_forms.py:752 #: netbox/templates/ipam/servicetemplate.html:12 msgid "Service Template" msgstr "" -#: netbox/ipam/forms/model_forms.py:791 +#: netbox/ipam/forms/model_forms.py:799 msgid "Port(s)" msgstr "" -#: netbox/ipam/forms/model_forms.py:805 +#: netbox/ipam/forms/model_forms.py:813 msgid "Service template" msgstr "" -#: netbox/ipam/forms/model_forms.py:817 +#: netbox/ipam/forms/model_forms.py:825 msgid "From Template" msgstr "" -#: netbox/ipam/forms/model_forms.py:818 +#: netbox/ipam/forms/model_forms.py:826 msgid "Custom" msgstr "" -#: netbox/ipam/forms/model_forms.py:848 +#: netbox/ipam/forms/model_forms.py:856 msgid "" "Must specify name, protocol, and port(s) if not using a service template." msgstr "" From 7aba6500ddef8a58d8d914b9899ee987a4a39a94 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 26 Feb 2025 13:43:04 -0500 Subject: [PATCH 15/38] Fixes #18141: Fix quick-add support for plugin models --- netbox/utilities/forms/fields/dynamic.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/netbox/utilities/forms/fields/dynamic.py b/netbox/utilities/forms/fields/dynamic.py index 793494b4b..67e1507f0 100644 --- a/netbox/utilities/forms/fields/dynamic.py +++ b/netbox/utilities/forms/fields/dynamic.py @@ -2,7 +2,7 @@ import django_filters from django import forms from django.conf import settings from django.forms import BoundField -from django.urls import reverse, reverse_lazy +from django.urls import reverse from utilities.forms import widgets from utilities.views import get_viewname @@ -171,10 +171,8 @@ class DynamicModelChoiceMixin: # Include quick add? if self.quick_add: - app_label = self.model._meta.app_label - model_name = self.model._meta.model_name widget.quick_add_context = { - 'url': reverse_lazy(f'{app_label}:{model_name}_add'), + 'url': reverse(get_viewname(self.model, 'add')), 'params': {}, } for k, v in self.quick_add_params.items(): From 09d867adc3fe8014b547d752e1cca3bf0b1eabdf Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Fri, 28 Feb 2025 09:28:09 -0600 Subject: [PATCH 16/38] Fixes #18758: Enable sorting by Account count on prodiver list (#18763) --- netbox/circuits/tables/providers.py | 1 - netbox/circuits/views.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/circuits/tables/providers.py b/netbox/circuits/tables/providers.py index c7eba9012..54a5c2cc9 100644 --- a/netbox/circuits/tables/providers.py +++ b/netbox/circuits/tables/providers.py @@ -23,7 +23,6 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable): verbose_name=_('Accounts') ) account_count = columns.LinkedCountColumn( - accessor=tables.A('accounts__count'), viewname='circuits:provideraccount_list', url_params={'provider_id': 'pk'}, verbose_name=_('Account Count') diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 07c1113bd..766a54bb8 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -23,6 +23,7 @@ class ProviderListView(generic.ObjectListView): queryset = Provider.objects.annotate( count_circuits=count_related(Circuit, 'provider'), asn_count=count_related(ASN, 'providers'), + account_count=count_related(ProviderAccount, 'provider'), ) filterset = filtersets.ProviderFilterSet filterset_form = forms.ProviderFilterForm From 63a167f1302582b065be74a6021f828e6404d7e4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 28 Feb 2025 11:01:48 -0500 Subject: [PATCH 17/38] Fixes #15924: Fix API interface patch tagged all mode (#18759) * Fixes: #15924 - Prevent API payload from allowing tagged_vlans while interface mode is set to taged-all * Prevent cleanup of tagged_vlans when no tagged_vlans set on interface * Fix test errors * Remove accidental debug statements * Update validation to model clean method instead of serializer * Remove clearing of tagged vlans from `save()` * Make changes to validation to account for M2M not being available under model in addition to not being able to check incoming vlans under same model. * Optimize untagged vlan check * Re-ordering statements in validators * Forgot to call super().clean() * Adjust logic for form and serializer. Add tests * Fix test failure * Fix ruff errors * Fix test by removing now invalid test * Update serializer, form and tests * Optimize API test for vlan fields * Optimize API serializer logic --------- Co-authored-by: Daniel Sheppard --- .../api/serializers_/device_components.py | 51 +++++- netbox/dcim/forms/common.py | 18 +- netbox/dcim/models/device_components.py | 2 + netbox/dcim/tests/test_api.py | 70 ++++++++ netbox/dcim/tests/test_forms.py | 166 +++++++++++++++++- 5 files changed, 292 insertions(+), 15 deletions(-) diff --git a/netbox/dcim/api/serializers_/device_components.py b/netbox/dcim/api/serializers_/device_components.py index a6767bb6f..b591030aa 100644 --- a/netbox/dcim/api/serializers_/device_components.py +++ b/netbox/dcim/api/serializers_/device_components.py @@ -1,3 +1,4 @@ +from django.utils.translation import gettext as _ from django.contrib.contenttypes.models import ContentType from drf_spectacular.utils import extend_schema_field from rest_framework import serializers @@ -232,8 +233,56 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect def validate(self, data): - # Validate many-to-many VLAN assignments if not self.nested: + + # Validate 802.1q mode and vlan(s) + mode = None + tagged_vlans = [] + + # Gather Information + if self.instance: + mode = data.get('mode') if 'mode' in data.keys() else self.instance.mode + untagged_vlan = data.get('untagged_vlan') if 'untagged_vlan' in data.keys() else \ + self.instance.untagged_vlan + qinq_svlan = data.get('qinq_svlan') if 'qinq_svlan' in data.keys() else \ + self.instance.qinq_svlan + tagged_vlans = data.get('tagged_vlans') if 'tagged_vlans' in data.keys() else \ + self.instance.tagged_vlans.all() + else: + mode = data.get('mode', None) + untagged_vlan = data.get('untagged_vlan') if 'untagged_vlan' in data.keys() else None + qinq_svlan = data.get('qinq_svlan') if 'qinq_svlan' in data.keys() else None + tagged_vlans = data.get('tagged_vlans') if 'tagged_vlans' in data.keys() else None + + errors = {} + + # Non Q-in-Q mode with service vlan set + if mode != InterfaceModeChoices.MODE_Q_IN_Q and qinq_svlan: + errors.update({ + 'qinq_svlan': _("Interface mode does not support q-in-q service vlan") + }) + # Routed mode + if not mode: + # Untagged vlan + if untagged_vlan: + errors.update({ + 'untagged_vlan': _("Interface mode does not support untagged vlan") + }) + # Tagged vlan + if tagged_vlans: + errors.update({ + 'tagged_vlans': _("Interface mode does not support tagged vlans") + }) + # Non-tagged mode + elif mode in (InterfaceModeChoices.MODE_TAGGED_ALL, InterfaceModeChoices.MODE_ACCESS) and tagged_vlans: + errors.update({ + 'tagged_vlans': _("Interface mode does not support tagged vlans") + }) + + if errors: + raise serializers.ValidationError(errors) + + # Validate many-to-many VLAN assignments device = self.instance.device if self.instance else data.get('device') for vlan in data.get('tagged_vlans', []): if vlan.site not in [device.site, None]: diff --git a/netbox/dcim/forms/common.py b/netbox/dcim/forms/common.py index 8ca258f34..23109f66b 100644 --- a/netbox/dcim/forms/common.py +++ b/netbox/dcim/forms/common.py @@ -43,20 +43,14 @@ class InterfaceCommonForm(forms.Form): super().clean() parent_field = 'device' if 'device' in self.cleaned_data else 'virtual_machine' - tagged_vlans = self.cleaned_data.get('tagged_vlans') - - # Untagged interfaces cannot be assigned tagged VLANs - if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and tagged_vlans: - raise forms.ValidationError({ - 'mode': _("An access interface cannot have tagged VLANs assigned.") - }) - - # Remove all tagged VLAN assignments from "tagged all" interfaces - elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED_ALL: - self.cleaned_data['tagged_vlans'] = [] + if 'tagged_vlans' in self.fields.keys(): + tagged_vlans = self.cleaned_data.get('tagged_vlans') if self.is_bound else \ + self.get_initial_for_field(self.fields['tagged_vlans'], 'tagged_vlans') + else: + tagged_vlans = [] # Validate tagged VLANs; must be a global VLAN or in the same site - elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED and tagged_vlans: + if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED and tagged_vlans: valid_sites = [None, self.cleaned_data[parent_field].site] invalid_vlans = [str(v) for v in tagged_vlans if v.site not in valid_sites] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index ce9e5607f..8a8e8f4cc 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -934,6 +934,8 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd raise ValidationError({'rf_channel_width': _("Cannot specify custom width with channel selected.")}) # VLAN validation + if not self.mode and self.untagged_vlan: + raise ValidationError({'untagged_vlan': _("Interface mode does not support an untagged vlan.")}) # Validate untagged VLAN if self.untagged_vlan and self.untagged_vlan.site not in [self.device.site, None]: diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 99a446aef..08f93f6ea 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -1,3 +1,5 @@ +import json + from django.test import override_settings from django.urls import reverse from django.utils.translation import gettext as _ @@ -1748,6 +1750,23 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase }, ] + def _perform_interface_test_with_invalid_data(self, mode: str = None, invalid_data: dict = {}): + device = Device.objects.first() + data = { + 'device': device.pk, + 'name': 'Interface 1', + 'type': InterfaceTypeChoices.TYPE_1GE_FIXED, + } + data.update({'mode': mode}) + data.update(invalid_data) + + response = self.client.post(self._get_list_url(), data, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) + content = json.loads(response.content) + for key in invalid_data.keys(): + self.assertIn(key, content) + self.assertIsNone(content.get('data')) + def test_bulk_delete_child_interfaces(self): interface1 = Interface.objects.get(name='Interface 1') device = interface1.device @@ -1775,6 +1794,57 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase self.client.delete(self._get_list_url(), data, format='json', **self.header) self.assertEqual(device.interfaces.count(), 2) # Child & parent were both deleted + def test_create_child_interfaces_mode_invalid_data(self): + """ + POST data to test interface mode check and invalid tagged/untagged VLANS. + """ + self.add_permissions('dcim.add_interface') + + vlans = VLAN.objects.all()[0:3] + + # Routed mode, untagged, tagged and qinq service vlan + invalid_data = { + 'untagged_vlan': vlans[0].pk, + 'tagged_vlans': [vlans[1].pk, vlans[2].pk], + 'qinq_svlan': vlans[2].pk + } + self._perform_interface_test_with_invalid_data(None, invalid_data) + + # Routed mode, untagged and tagged vlan + invalid_data = { + 'untagged_vlan': vlans[0].pk, + 'tagged_vlans': [vlans[1].pk, vlans[2].pk], + } + self._perform_interface_test_with_invalid_data(None, invalid_data) + + # Routed mode, untagged vlan + invalid_data = { + 'untagged_vlan': vlans[0].pk, + } + self._perform_interface_test_with_invalid_data(None, invalid_data) + + invalid_data = { + 'tagged_vlans': [vlans[1].pk, vlans[2].pk], + } + # Routed mode, qinq service vlan + self._perform_interface_test_with_invalid_data(None, invalid_data) + # Access mode, tagged vlans + self._perform_interface_test_with_invalid_data(InterfaceModeChoices.MODE_ACCESS, invalid_data) + # All tagged mode, tagged vlans + self._perform_interface_test_with_invalid_data(InterfaceModeChoices.MODE_TAGGED_ALL, invalid_data) + + invalid_data = { + 'qinq_svlan': vlans[0].pk, + } + # Routed mode, qinq service vlan + self._perform_interface_test_with_invalid_data(None, invalid_data) + # Access mode, qinq service vlan + self._perform_interface_test_with_invalid_data(InterfaceModeChoices.MODE_ACCESS, invalid_data) + # Tagged mode, qinq service vlan + self._perform_interface_test_with_invalid_data(InterfaceModeChoices.MODE_TAGGED, invalid_data) + # Tagged-all mode, qinq service vlan + self._perform_interface_test_with_invalid_data(InterfaceModeChoices.MODE_TAGGED_ALL, invalid_data) + class FrontPortTest(APIViewTestCases.APIViewTestCase): model = FrontPort diff --git a/netbox/dcim/tests/test_forms.py b/netbox/dcim/tests/test_forms.py index 7a57bf3f0..89b7508f3 100644 --- a/netbox/dcim/tests/test_forms.py +++ b/netbox/dcim/tests/test_forms.py @@ -1,8 +1,9 @@ from django.test import TestCase -from dcim.choices import DeviceFaceChoices, DeviceStatusChoices, InterfaceTypeChoices +from dcim.choices import DeviceFaceChoices, DeviceStatusChoices, InterfaceTypeChoices, InterfaceModeChoices from dcim.forms import * from dcim.models import * +from ipam.models import VLAN from utilities.testing import create_test_device from virtualization.models import Cluster, ClusterGroup, ClusterType @@ -117,11 +118,23 @@ class DeviceTestCase(TestCase): self.assertIn('position', form.errors) -class LabelTestCase(TestCase): +class InterfaceTestCase(TestCase): @classmethod def setUpTestData(cls): cls.device = create_test_device('Device 1') + cls.vlans = ( + VLAN(name='VLAN 1', vid=1), + VLAN(name='VLAN 2', vid=2), + VLAN(name='VLAN 3', vid=3), + ) + VLAN.objects.bulk_create(cls.vlans) + cls.interface = Interface.objects.create( + device=cls.device, + name='Interface 1', + type=InterfaceTypeChoices.TYPE_1GE_GBIC, + mode=InterfaceModeChoices.MODE_TAGGED, + ) def test_interface_label_count_valid(self): """ @@ -151,3 +164,152 @@ class LabelTestCase(TestCase): self.assertFalse(form.is_valid()) self.assertIn('label', form.errors) + + def test_create_interface_mode_valid_data(self): + """ + Test that saving valid interface mode and tagged/untagged vlans works properly + """ + + # Validate access mode + data = { + 'device': self.device.pk, + 'name': 'ethernet1/1', + 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, + 'mode': InterfaceModeChoices.MODE_ACCESS, + 'untagged_vlan': self.vlans[0].pk + } + form = InterfaceCreateForm(data) + + self.assertTrue(form.is_valid()) + + # Validate tagged vlans + data = { + 'device': self.device.pk, + 'name': 'ethernet1/2', + 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, + 'mode': InterfaceModeChoices.MODE_TAGGED, + 'untagged_vlan': self.vlans[0].pk, + 'tagged_vlans': [self.vlans[1].pk, self.vlans[2].pk] + } + form = InterfaceCreateForm(data) + self.assertTrue(form.is_valid()) + + # Validate tagged vlans + data = { + 'device': self.device.pk, + 'name': 'ethernet1/3', + 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, + 'mode': InterfaceModeChoices.MODE_TAGGED_ALL, + 'untagged_vlan': self.vlans[0].pk, + } + form = InterfaceCreateForm(data) + self.assertTrue(form.is_valid()) + + def test_create_interface_mode_access_invalid_data(self): + """ + Test that saving invalid interface mode and tagged/untagged vlans works properly + """ + data = { + 'device': self.device.pk, + 'name': 'ethernet1/4', + 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, + 'mode': InterfaceModeChoices.MODE_ACCESS, + 'untagged_vlan': self.vlans[0].pk, + 'tagged_vlans': [self.vlans[1].pk, self.vlans[2].pk] + } + form = InterfaceCreateForm(data) + + self.assertTrue(form.is_valid()) + self.assertIn('untagged_vlan', form.cleaned_data.keys()) + self.assertNotIn('tagged_vlans', form.cleaned_data.keys()) + self.assertNotIn('qinq_svlan', form.cleaned_data.keys()) + + def test_edit_interface_mode_access_invalid_data(self): + """ + Test that saving invalid interface mode and tagged/untagged vlans works properly + """ + data = { + 'device': self.device.pk, + 'name': 'Ethernet 1/5', + 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, + 'mode': InterfaceModeChoices.MODE_ACCESS, + 'tagged_vlans': [self.vlans[0].pk, self.vlans[1].pk, self.vlans[2].pk] + } + form = InterfaceForm(data, instance=self.interface) + + self.assertTrue(form.is_valid()) + self.assertIn('untagged_vlan', form.cleaned_data.keys()) + self.assertNotIn('tagged_vlans', form.cleaned_data.keys()) + self.assertNotIn('qinq_svlan', form.cleaned_data.keys()) + + def test_create_interface_mode_tagged_all_invalid_data(self): + """ + Test that saving invalid interface mode and tagged/untagged vlans works properly + """ + data = { + 'device': self.device.pk, + 'name': 'ethernet1/6', + 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, + 'mode': InterfaceModeChoices.MODE_TAGGED_ALL, + 'tagged_vlans': [self.vlans[0].pk, self.vlans[1].pk, self.vlans[2].pk] + } + form = InterfaceCreateForm(data) + + self.assertTrue(form.is_valid()) + self.assertIn('untagged_vlan', form.cleaned_data.keys()) + self.assertNotIn('tagged_vlans', form.cleaned_data.keys()) + self.assertNotIn('qinq_svlan', form.cleaned_data.keys()) + + def test_edit_interface_mode_tagged_all_invalid_data(self): + """ + Test that saving invalid interface mode and tagged/untagged vlans works properly + """ + data = { + 'device': self.device.pk, + 'name': 'Ethernet 1/7', + 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, + 'mode': InterfaceModeChoices.MODE_TAGGED_ALL, + 'tagged_vlans': [self.vlans[0].pk, self.vlans[1].pk, self.vlans[2].pk] + } + form = InterfaceForm(data) + self.assertTrue(form.is_valid()) + self.assertIn('untagged_vlan', form.cleaned_data.keys()) + self.assertNotIn('tagged_vlans', form.cleaned_data.keys()) + self.assertNotIn('qinq_svlan', form.cleaned_data.keys()) + + def test_create_interface_mode_routed_invalid_data(self): + """ + Test that saving invalid interface mode (routed) and tagged/untagged vlans works properly + """ + data = { + 'device': self.device.pk, + 'name': 'ethernet1/6', + 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, + 'mode': None, + 'untagged_vlan': self.vlans[0].pk, + 'tagged_vlans': [self.vlans[0].pk, self.vlans[1].pk, self.vlans[2].pk] + } + form = InterfaceCreateForm(data) + + self.assertTrue(form.is_valid()) + self.assertNotIn('untagged_vlan', form.cleaned_data.keys()) + self.assertNotIn('tagged_vlans', form.cleaned_data.keys()) + self.assertNotIn('qinq_svlan', form.cleaned_data.keys()) + + def test_edit_interface_mode_routed_invalid_data(self): + """ + Test that saving invalid interface mode (routed) and tagged/untagged vlans works properly + """ + data = { + 'device': self.device.pk, + 'name': 'Ethernet 1/7', + 'type': InterfaceTypeChoices.TYPE_1GE_GBIC, + 'mode': None, + 'untagged_vlan': self.vlans[0].pk, + 'tagged_vlans': [self.vlans[0].pk, self.vlans[1].pk, self.vlans[2].pk] + } + form = InterfaceForm(data) + self.assertTrue(form.is_valid()) + self.assertNotIn('untagged_vlan', form.cleaned_data.keys()) + self.assertNotIn('tagged_vlans', form.cleaned_data.keys()) + self.assertNotIn('qinq_svlan', form.cleaned_data.keys()) From 3b1daaaad66e76219c1dff9f893297aae9ea9a30 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 1 Mar 2025 05:02:06 +0000 Subject: [PATCH 18/38] Update source translation strings --- netbox/translations/en/LC_MESSAGES/django.po | 149 +++++++++++-------- 1 file changed, 83 insertions(+), 66 deletions(-) diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index 7f6aef08a..67426b27c 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-02-26 05:01+0000\n" +"POT-Creation-Date: 2025-03-01 05:01+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -442,7 +442,7 @@ msgstr "" #: netbox/circuits/forms/bulk_edit.py:42 netbox/circuits/forms/filtersets.py:64 #: netbox/circuits/forms/model_forms.py:42 -#: netbox/circuits/tables/providers.py:33 netbox/dcim/forms/bulk_edit.py:132 +#: netbox/circuits/tables/providers.py:32 netbox/dcim/forms/bulk_edit.py:132 #: netbox/dcim/forms/filtersets.py:196 netbox/dcim/forms/model_forms.py:127 #: netbox/dcim/tables/sites.py:94 netbox/ipam/models/asns.py:123 #: netbox/ipam/tables/asn.py:27 netbox/ipam/views.py:230 @@ -613,8 +613,8 @@ msgstr "" #: netbox/circuits/forms/model_forms.py:110 #: netbox/circuits/tables/circuits.py:57 netbox/circuits/tables/circuits.py:112 #: netbox/circuits/tables/circuits.py:196 -#: netbox/circuits/tables/providers.py:71 -#: netbox/circuits/tables/providers.py:102 +#: netbox/circuits/tables/providers.py:70 +#: netbox/circuits/tables/providers.py:101 #: netbox/circuits/tables/virtual_circuits.py:46 #: netbox/circuits/tables/virtual_circuits.py:93 #: netbox/templates/circuits/circuit.html:18 @@ -1317,7 +1317,7 @@ msgid "Site group" msgstr "" #: netbox/circuits/forms/filtersets.py:81 netbox/circuits/tables/circuits.py:62 -#: netbox/circuits/tables/providers.py:65 +#: netbox/circuits/tables/providers.py:64 #: netbox/circuits/tables/virtual_circuits.py:55 #: netbox/circuits/tables/virtual_circuits.py:103 #: netbox/templates/circuits/circuit.html:22 @@ -1401,9 +1401,9 @@ msgstr "" #: netbox/dcim/models/device_component_templates.py:531 #: netbox/dcim/models/device_component_templates.py:631 #: netbox/dcim/models/device_components.py:476 -#: netbox/dcim/models/device_components.py:1024 -#: netbox/dcim/models/device_components.py:1095 -#: netbox/dcim/models/device_components.py:1241 +#: netbox/dcim/models/device_components.py:1026 +#: netbox/dcim/models/device_components.py:1097 +#: netbox/dcim/models/device_components.py:1243 #: netbox/dcim/models/devices.py:478 netbox/dcim/models/racks.py:221 #: netbox/extras/models/tags.py:28 msgid "color" @@ -1430,7 +1430,7 @@ msgstr "" #: netbox/circuits/models/circuits.py:67 #: netbox/circuits/models/virtual_circuits.py:59 netbox/core/models/data.py:52 #: netbox/core/models/jobs.py:85 netbox/dcim/models/cables.py:49 -#: netbox/dcim/models/device_components.py:1281 +#: netbox/dcim/models/device_components.py:1283 #: netbox/dcim/models/devices.py:645 netbox/dcim/models/devices.py:1181 #: netbox/dcim/models/devices.py:1409 netbox/dcim/models/power.py:94 #: netbox/dcim/models/racks.py:288 netbox/dcim/models/sites.py:154 @@ -1668,8 +1668,8 @@ msgstr "" #: netbox/circuits/tables/circuits.py:30 netbox/circuits/tables/circuits.py:168 #: netbox/circuits/tables/providers.py:18 -#: netbox/circuits/tables/providers.py:68 -#: netbox/circuits/tables/providers.py:98 +#: netbox/circuits/tables/providers.py:67 +#: netbox/circuits/tables/providers.py:97 #: netbox/circuits/tables/virtual_circuits.py:18 netbox/core/tables/data.py:16 #: netbox/core/tables/jobs.py:14 netbox/core/tables/plugins.py:44 #: netbox/core/tables/tasks.py:11 netbox/core/tables/tasks.py:115 @@ -1789,8 +1789,8 @@ msgid "Name" msgstr "" #: netbox/circuits/tables/circuits.py:39 netbox/circuits/tables/circuits.py:174 -#: netbox/circuits/tables/providers.py:44 -#: netbox/circuits/tables/providers.py:78 +#: netbox/circuits/tables/providers.py:43 +#: netbox/circuits/tables/providers.py:77 #: netbox/circuits/tables/virtual_circuits.py:27 #: netbox/netbox/navigation/menu.py:274 netbox/netbox/navigation/menu.py:278 #: netbox/netbox/navigation/menu.py:280 @@ -1822,9 +1822,9 @@ msgstr "" msgid "Commit Rate" msgstr "" -#: netbox/circuits/tables/circuits.py:84 netbox/circuits/tables/providers.py:47 -#: netbox/circuits/tables/providers.py:81 -#: netbox/circuits/tables/providers.py:106 +#: netbox/circuits/tables/circuits.py:84 netbox/circuits/tables/providers.py:46 +#: netbox/circuits/tables/providers.py:80 +#: netbox/circuits/tables/providers.py:105 #: netbox/circuits/tables/virtual_circuits.py:68 #: netbox/dcim/tables/devices.py:1068 netbox/dcim/tables/devicetypes.py:97 #: netbox/dcim/tables/modules.py:29 netbox/dcim/tables/modules.py:73 @@ -1887,11 +1887,11 @@ msgstr "" msgid "Accounts" msgstr "" -#: netbox/circuits/tables/providers.py:29 +#: netbox/circuits/tables/providers.py:28 msgid "Account Count" msgstr "" -#: netbox/circuits/tables/providers.py:38 netbox/dcim/tables/sites.py:99 +#: netbox/circuits/tables/providers.py:37 netbox/dcim/tables/sites.py:99 msgid "ASN Count" msgstr "" @@ -1973,12 +1973,12 @@ msgstr "" msgid "Device" msgstr "" -#: netbox/circuits/views.py:355 +#: netbox/circuits/views.py:356 #, python-brace-format msgid "No terminations have been defined for circuit {circuit}." msgstr "" -#: netbox/circuits/views.py:404 +#: netbox/circuits/views.py:405 #, python-brace-format msgid "Swapped terminations for circuit {circuit}." msgstr "" @@ -2507,8 +2507,8 @@ msgstr "" #: netbox/dcim/models/device_components.py:337 #: netbox/dcim/models/device_components.py:453 #: netbox/dcim/models/device_components.py:653 -#: netbox/dcim/models/device_components.py:1019 -#: netbox/dcim/models/device_components.py:1090 netbox/dcim/models/power.py:100 +#: netbox/dcim/models/device_components.py:1021 +#: netbox/dcim/models/device_components.py:1092 netbox/dcim/models/power.py:100 #: netbox/extras/models/customfields.py:78 netbox/extras/models/search.py:41 #: netbox/virtualization/models/clusters.py:57 netbox/vpn/models/l2vpn.py:32 msgid "type" @@ -2913,6 +2913,19 @@ msgstr "" msgid "Plugin {name} not found" msgstr "" +#: netbox/dcim/api/serializers_/device_components.py:262 +msgid "Interface mode does not support q-in-q service vlan" +msgstr "" + +#: netbox/dcim/api/serializers_/device_components.py:269 +msgid "Interface mode does not support untagged vlan" +msgstr "" + +#: netbox/dcim/api/serializers_/device_components.py:274 +#: netbox/dcim/api/serializers_/device_components.py:279 +msgid "Interface mode does not support tagged vlans" +msgstr "" + #: netbox/dcim/api/serializers_/devices.py:53 #: netbox/dcim/api/serializers_/devicetypes.py:26 msgid "Position (U)" @@ -4476,7 +4489,7 @@ msgstr "" msgid "Interface mode must be specified to assign VLANs" msgstr "" -#: netbox/dcim/forms/bulk_edit.py:1639 netbox/dcim/forms/common.py:51 +#: netbox/dcim/forms/bulk_edit.py:1639 msgid "An access interface cannot have tagged VLANs assigned." msgstr "" @@ -4987,32 +5000,32 @@ msgstr "" msgid "MTU" msgstr "" -#: netbox/dcim/forms/common.py:66 +#: netbox/dcim/forms/common.py:60 #, python-brace-format msgid "" "The tagged VLANs ({vlans}) must belong to the same site as the interface's " "parent device/VM, or they must be global" msgstr "" -#: netbox/dcim/forms/common.py:127 +#: netbox/dcim/forms/common.py:121 msgid "" "Cannot install module with placeholder values in a module bay with no " "position defined." msgstr "" -#: netbox/dcim/forms/common.py:133 +#: netbox/dcim/forms/common.py:127 #, python-brace-format msgid "" "Cannot install module with placeholder values in a module bay tree {level} " "in tree but {tokens} placeholders given." msgstr "" -#: netbox/dcim/forms/common.py:148 +#: netbox/dcim/forms/common.py:142 #, python-brace-format msgid "Cannot adopt {model} {name} as it already belongs to a module" msgstr "" -#: netbox/dcim/forms/common.py:157 +#: netbox/dcim/forms/common.py:151 #, python-brace-format msgid "A {model} named {name} already exists" msgstr "" @@ -5718,7 +5731,7 @@ msgid "Bridge interface ({bridge}) must belong to the same module type" msgstr "" #: netbox/dcim/models/device_component_templates.py:540 -#: netbox/dcim/models/device_components.py:1033 +#: netbox/dcim/models/device_components.py:1035 msgid "rear port position" msgstr "" @@ -5743,7 +5756,7 @@ msgid "" msgstr "" #: netbox/dcim/models/device_component_templates.py:635 -#: netbox/dcim/models/device_components.py:1099 +#: netbox/dcim/models/device_components.py:1101 msgid "positions" msgstr "" @@ -5756,12 +5769,12 @@ msgid "rear port templates" msgstr "" #: netbox/dcim/models/device_component_templates.py:676 -#: netbox/dcim/models/device_components.py:1146 +#: netbox/dcim/models/device_components.py:1148 msgid "position" msgstr "" #: netbox/dcim/models/device_component_templates.py:679 -#: netbox/dcim/models/device_components.py:1149 +#: netbox/dcim/models/device_components.py:1151 msgid "Identifier to reference when renaming installed components" msgstr "" @@ -5789,12 +5802,12 @@ msgid "" msgstr "" #: netbox/dcim/models/device_component_templates.py:784 -#: netbox/dcim/models/device_components.py:1302 +#: netbox/dcim/models/device_components.py:1304 msgid "part ID" msgstr "" #: netbox/dcim/models/device_component_templates.py:786 -#: netbox/dcim/models/device_components.py:1304 +#: netbox/dcim/models/device_components.py:1306 msgid "Manufacturer-assigned part identifier" msgstr "" @@ -6090,146 +6103,150 @@ msgstr "" msgid "Cannot specify custom width with channel selected." msgstr "" -#: netbox/dcim/models/device_components.py:942 +#: netbox/dcim/models/device_components.py:938 +msgid "Interface mode does not support an untagged vlan." +msgstr "" + +#: netbox/dcim/models/device_components.py:944 #, python-brace-format msgid "" "The untagged VLAN ({untagged_vlan}) must belong to the same site as the " "interface's parent device, or it must be global." msgstr "" -#: netbox/dcim/models/device_components.py:1039 +#: netbox/dcim/models/device_components.py:1041 msgid "Mapped position on corresponding rear port" msgstr "" -#: netbox/dcim/models/device_components.py:1055 +#: netbox/dcim/models/device_components.py:1057 msgid "front port" msgstr "" -#: netbox/dcim/models/device_components.py:1056 +#: netbox/dcim/models/device_components.py:1058 msgid "front ports" msgstr "" -#: netbox/dcim/models/device_components.py:1067 +#: netbox/dcim/models/device_components.py:1069 #, python-brace-format msgid "Rear port ({rear_port}) must belong to the same device" msgstr "" -#: netbox/dcim/models/device_components.py:1075 +#: netbox/dcim/models/device_components.py:1077 #, python-brace-format msgid "" "Invalid rear port position ({rear_port_position}): Rear port {name} has only " "{positions} positions." msgstr "" -#: netbox/dcim/models/device_components.py:1105 +#: netbox/dcim/models/device_components.py:1107 msgid "Number of front ports which may be mapped" msgstr "" -#: netbox/dcim/models/device_components.py:1110 +#: netbox/dcim/models/device_components.py:1112 msgid "rear port" msgstr "" -#: netbox/dcim/models/device_components.py:1111 +#: netbox/dcim/models/device_components.py:1113 msgid "rear ports" msgstr "" -#: netbox/dcim/models/device_components.py:1122 +#: netbox/dcim/models/device_components.py:1124 #, python-brace-format msgid "" "The number of positions cannot be less than the number of mapped front ports " "({frontport_count})" msgstr "" -#: netbox/dcim/models/device_components.py:1163 +#: netbox/dcim/models/device_components.py:1165 msgid "module bay" msgstr "" -#: netbox/dcim/models/device_components.py:1164 +#: netbox/dcim/models/device_components.py:1166 msgid "module bays" msgstr "" -#: netbox/dcim/models/device_components.py:1178 +#: netbox/dcim/models/device_components.py:1180 #: netbox/dcim/models/devices.py:1229 msgid "A module bay cannot belong to a module installed within it." msgstr "" -#: netbox/dcim/models/device_components.py:1204 +#: netbox/dcim/models/device_components.py:1206 msgid "device bay" msgstr "" -#: netbox/dcim/models/device_components.py:1205 +#: netbox/dcim/models/device_components.py:1207 msgid "device bays" msgstr "" -#: netbox/dcim/models/device_components.py:1212 +#: netbox/dcim/models/device_components.py:1214 #, python-brace-format msgid "This type of device ({device_type}) does not support device bays." msgstr "" -#: netbox/dcim/models/device_components.py:1218 +#: netbox/dcim/models/device_components.py:1220 msgid "Cannot install a device into itself." msgstr "" -#: netbox/dcim/models/device_components.py:1226 +#: netbox/dcim/models/device_components.py:1228 #, python-brace-format msgid "" "Cannot install the specified device; device is already installed in {bay}." msgstr "" -#: netbox/dcim/models/device_components.py:1247 +#: netbox/dcim/models/device_components.py:1249 msgid "inventory item role" msgstr "" -#: netbox/dcim/models/device_components.py:1248 +#: netbox/dcim/models/device_components.py:1250 msgid "inventory item roles" msgstr "" -#: netbox/dcim/models/device_components.py:1308 +#: netbox/dcim/models/device_components.py:1310 #: netbox/dcim/models/devices.py:598 netbox/dcim/models/devices.py:1189 #: netbox/dcim/models/racks.py:304 #: netbox/virtualization/models/virtualmachines.py:126 msgid "serial number" msgstr "" -#: netbox/dcim/models/device_components.py:1316 +#: netbox/dcim/models/device_components.py:1318 #: netbox/dcim/models/devices.py:606 netbox/dcim/models/devices.py:1196 #: netbox/dcim/models/racks.py:311 msgid "asset tag" msgstr "" -#: netbox/dcim/models/device_components.py:1317 +#: netbox/dcim/models/device_components.py:1319 msgid "A unique tag used to identify this item" msgstr "" -#: netbox/dcim/models/device_components.py:1320 +#: netbox/dcim/models/device_components.py:1322 msgid "discovered" msgstr "" -#: netbox/dcim/models/device_components.py:1322 +#: netbox/dcim/models/device_components.py:1324 msgid "This item was automatically discovered" msgstr "" -#: netbox/dcim/models/device_components.py:1340 +#: netbox/dcim/models/device_components.py:1342 msgid "inventory item" msgstr "" -#: netbox/dcim/models/device_components.py:1341 +#: netbox/dcim/models/device_components.py:1343 msgid "inventory items" msgstr "" -#: netbox/dcim/models/device_components.py:1349 +#: netbox/dcim/models/device_components.py:1351 msgid "Cannot assign self as parent." msgstr "" -#: netbox/dcim/models/device_components.py:1357 +#: netbox/dcim/models/device_components.py:1359 msgid "Parent inventory item does not belong to the same device." msgstr "" -#: netbox/dcim/models/device_components.py:1363 +#: netbox/dcim/models/device_components.py:1365 msgid "Cannot move an inventory item with dependent children" msgstr "" -#: netbox/dcim/models/device_components.py:1371 +#: netbox/dcim/models/device_components.py:1373 msgid "Cannot assign inventory item to component on another device" msgstr "" @@ -7374,7 +7391,7 @@ msgstr "" msgid "Sites" msgstr "" -#: netbox/dcim/tests/test_api.py:48 +#: netbox/dcim/tests/test_api.py:50 msgid "Test case must set peer_termination_type" msgstr "" From 06b5ff2e4ae3a38db3576b00800076264b80dbad Mon Sep 17 00:00:00 2001 From: James Raphael Tiovalen Date: Sun, 2 Mar 2025 18:40:40 +0800 Subject: [PATCH 19/38] Closes #18774: Set title attribute of each tag to its description --- netbox/utilities/templates/builtins/tag.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/utilities/templates/builtins/tag.html b/netbox/utilities/templates/builtins/tag.html index d63b6afa6..48f269388 100644 --- a/netbox/utilities/templates/builtins/tag.html +++ b/netbox/utilities/templates/builtins/tag.html @@ -1,3 +1,3 @@ {% load helpers %} -{% if viewname %}{% endif %}{{ tag }}{% if viewname %}{% endif %} +{% if viewname %}{% endif %}{{ tag }}{% if viewname %}{% endif %} From 3e1d4369ba49f1f8e76ca59cc6f91d628696eea4 Mon Sep 17 00:00:00 2001 From: atownson <52260120+atownson@users.noreply.github.com> Date: Mon, 3 Mar 2025 08:20:04 -0600 Subject: [PATCH 20/38] Closes #17944: Allow filtering of ObjectVar and MultiObjectVar script inputs (#18725) * Add the advanced object selector to the ObjectVar and MultiObjectVar script inputs * Fix formatting issue --- docs/customization/custom-scripts.md | 1 + netbox/extras/scripts.py | 5 ++++- netbox/templates/extras/script.html | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/customization/custom-scripts.md b/docs/customization/custom-scripts.md index 1051b31f6..7774ef35f 100644 --- a/docs/customization/custom-scripts.md +++ b/docs/customization/custom-scripts.md @@ -308,6 +308,7 @@ A particular object within NetBox. Each ObjectVar must specify a particular mode * `query_params` - A dictionary of query parameters to use when retrieving available options (optional) * `context` - A custom dictionary mapping template context variables to fields, used when rendering `