From 0d29e5776c54fa0d447d6012810545cd25f429b9 Mon Sep 17 00:00:00 2001 From: Omri Abu <6192223+Omripresent@users.noreply.github.com> Date: Tue, 13 May 2025 22:20:54 -0400 Subject: [PATCH 01/30] Update get_for_virtualmachine to support lookup by cluster location scope Update test case to include location scoped cluster --- netbox/ipam/querysets.py | 26 +++++++++++++++++++++++++- netbox/ipam/tests/test_filtersets.py | 22 +++++++++++++++------- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/netbox/ipam/querysets.py b/netbox/ipam/querysets.py index 77ab8194a..89013aa31 100644 --- a/netbox/ipam/querysets.py +++ b/netbox/ipam/querysets.py @@ -148,7 +148,7 @@ class VLANQuerySet(RestrictedQuerySet): # Find all relevant VLANGroups q = Q() - site = vm.site or vm.cluster._site + site = vm.site if vm.cluster: # Add VLANGroups scoped to the assigned cluster (or its group) q |= Q( @@ -160,6 +160,30 @@ class VLANQuerySet(RestrictedQuerySet): scope_type=ContentType.objects.get_by_natural_key('virtualization', 'clustergroup'), scope_id=vm.cluster.group_id ) + # Looking all possible cluster scopes + if vm.cluster.scope_type == ContentType.objects.get_by_natural_key('dcim', 'location'): + site = site or vm.cluster.scope.site + q |= Q( + scope_type=ContentType.objects.get_by_natural_key('dcim', 'location'), + scope_id__in=vm.cluster.scope.get_ancestors(include_self=True) + ) + elif vm.cluster.scope_type == ContentType.objects.get_by_natural_key('dcim', 'site'): + site = site or vm.cluster.scope + q |= Q( + scope_type=ContentType.objects.get_by_natural_key('dcim', 'site'), + scope_id=vm.cluster.scope.pk + ) + elif vm.cluster.scope_type == ContentType.objects.get_by_natural_key('dcim', 'sitegroup'): + q |= Q( + scope_type=ContentType.objects.get_by_natural_key('dcim', 'sitegroup'), + scope_id__in=vm.cluster.scope.get_ancestors(include_self=True) + ) + elif vm.cluster.scope_type == ContentType.objects.get_by_natural_key('dcim', 'region'): + q |= Q( + scope_type=ContentType.objects.get_by_natural_key('dcim', 'region'), + scope_id__in=vm.cluster.scope.get_ancestors(include_self=True) + ) + # VM can be assigned to a site without a cluster so checking assigned site independently if site: # Add VLANGroups scoped to the assigned site (or its group or region) q |= Q( diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index c1b4acf60..852fd3ea9 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -1849,6 +1849,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests): Cluster(name='Cluster 1', type=cluster_type, group=cluster_groups[0], scope=sites[0]), Cluster(name='Cluster 2', type=cluster_type, group=cluster_groups[1], scope=sites[1]), Cluster(name='Cluster 3', type=cluster_type, group=cluster_groups[2], scope=sites[2]), + Cluster(name='Cluster 4', type=cluster_type, group=cluster_groups[0], scope=locations[0]), ) for cluster in clusters: cluster.save() @@ -1857,6 +1858,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests): VirtualMachine(name='Virtual Machine 1', cluster=clusters[0]), VirtualMachine(name='Virtual Machine 2', cluster=clusters[1]), VirtualMachine(name='Virtual Machine 3', cluster=clusters[2]), + VirtualMachine(name='Virtual Machine 4', cluster=clusters[3]), ) VirtualMachine.objects.bulk_create(virtual_machines) @@ -1864,6 +1866,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests): VMInterface(virtual_machine=virtual_machines[0], name='VM Interface 1'), VMInterface(virtual_machine=virtual_machines[1], name='VM Interface 2'), VMInterface(virtual_machine=virtual_machines[2], name='VM Interface 3'), + VMInterface(virtual_machine=virtual_machines[3], name='VM Interface 4'), ) VMInterface.objects.bulk_create(vm_interfaces) @@ -1890,6 +1893,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests): VLANGroup(name='Cluster 1', slug='cluster-1', scope=clusters[0]), VLANGroup(name='Cluster 2', slug='cluster-2', scope=clusters[1]), VLANGroup(name='Cluster 3', slug='cluster-3', scope=clusters[2]), + VLANGroup(name='Cluster 4', slug='cluster-4', scope=clusters[3]), # General purpose VLAN groups VLANGroup(name='VLAN Group 1', slug='vlan-group-1'), @@ -1944,11 +1948,12 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests): VLAN(vid=19, name='Cluster 1', group=groups[18]), VLAN(vid=20, name='Cluster 2', group=groups[19]), VLAN(vid=21, name='Cluster 3', group=groups[20]), + VLAN(vid=22, name='Cluster 4', group=groups[21]), VLAN( vid=101, name='VLAN 101', site=sites[3], - group=groups[21], + group=groups[22], role=roles[0], tenant=tenants[0], status=VLANStatusChoices.STATUS_ACTIVE, @@ -1957,7 +1962,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests): vid=102, name='VLAN 102', site=sites[3], - group=groups[21], + group=groups[22], role=roles[0], tenant=tenants[0], status=VLANStatusChoices.STATUS_ACTIVE, @@ -1966,7 +1971,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests): vid=201, name='VLAN 201', site=sites[4], - group=groups[22], + group=groups[23], role=roles[1], tenant=tenants[1], status=VLANStatusChoices.STATUS_DEPRECATED, @@ -1975,7 +1980,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests): vid=202, name='VLAN 202', site=sites[4], - group=groups[22], + group=groups[23], role=roles[1], tenant=tenants[1], status=VLANStatusChoices.STATUS_DEPRECATED, @@ -1984,7 +1989,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests): vid=301, name='VLAN 301', site=sites[5], - group=groups[23], + group=groups[24], role=roles[2], tenant=tenants[2], status=VLANStatusChoices.STATUS_RESERVED, @@ -1993,13 +1998,13 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests): vid=302, name='VLAN 302', site=sites[5], - group=groups[23], + group=groups[24], role=roles[2], tenant=tenants[2], status=VLANStatusChoices.STATUS_RESERVED, ), # Create one globally available VLAN on a VLAN group - VLAN(vid=500, name='VLAN Group 1', group=groups[24]), + VLAN(vid=500, name='VLAN Group 1', group=groups[25]), # Create one globally available VLAN VLAN(vid=1000, name='Global VLAN'), # Create some Q-in-Q service VLANs @@ -2130,6 +2135,9 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests): vm_id = VirtualMachine.objects.first().pk params = {'available_on_virtualmachine': vm_id} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7) # 5 scoped + 1 global group + 1 global + vm_id = VirtualMachine.objects.get(name='Virtual Machine 4').pk + params = {'available_on_virtualmachine': vm_id} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) # 6 scoped + 1 global group + 1 global def test_available_at_site(self): site_id = Site.objects.first().pk From b497b8566554d6b3d49e80dde86264e082bdfb11 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 05:02:15 +0000 Subject: [PATCH 02/30] Update source translation strings --- netbox/translations/en/LC_MESSAGES/django.po | 75 +++++++++----------- 1 file changed, 34 insertions(+), 41 deletions(-) diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index cf8b89645..a89bc7b78 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-05-13 05:01+0000\n" +"POT-Creation-Date: 2025-05-14 05:01+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -482,7 +482,7 @@ msgstr "" #: netbox/dcim/forms/bulk_edit.py:1194 netbox/dcim/forms/bulk_edit.py:1239 #: netbox/dcim/forms/bulk_edit.py:1266 netbox/dcim/forms/bulk_edit.py:1284 #: netbox/dcim/forms/bulk_edit.py:1302 netbox/dcim/forms/bulk_edit.py:1320 -#: netbox/dcim/forms/bulk_edit.py:1793 netbox/dcim/forms/bulk_edit.py:1834 +#: netbox/dcim/forms/bulk_edit.py:1800 netbox/dcim/forms/bulk_edit.py:1841 #: netbox/extras/forms/bulk_edit.py:40 netbox/extras/forms/bulk_edit.py:150 #: netbox/extras/forms/bulk_edit.py:183 netbox/extras/forms/bulk_edit.py:211 #: netbox/extras/forms/bulk_edit.py:241 netbox/extras/forms/bulk_edit.py:289 @@ -653,7 +653,7 @@ msgstr "" #: netbox/circuits/forms/filtersets.py:321 netbox/dcim/forms/bulk_edit.py:216 #: netbox/dcim/forms/bulk_edit.py:656 netbox/dcim/forms/bulk_edit.py:866 #: netbox/dcim/forms/bulk_edit.py:1235 netbox/dcim/forms/bulk_edit.py:1262 -#: netbox/dcim/forms/bulk_edit.py:1789 netbox/dcim/forms/filtersets.py:1132 +#: netbox/dcim/forms/bulk_edit.py:1796 netbox/dcim/forms/filtersets.py:1132 #: netbox/dcim/forms/filtersets.py:1390 netbox/dcim/forms/filtersets.py:1543 #: netbox/dcim/forms/filtersets.py:1567 netbox/dcim/tables/devices.py:744 #: netbox/dcim/tables/devices.py:800 netbox/dcim/tables/devices.py:1041 @@ -758,7 +758,7 @@ msgstr "" #: netbox/dcim/forms/bulk_edit.py:366 netbox/dcim/forms/bulk_edit.py:753 #: netbox/dcim/forms/bulk_edit.py:818 netbox/dcim/forms/bulk_edit.py:850 #: netbox/dcim/forms/bulk_edit.py:977 netbox/dcim/forms/bulk_edit.py:1770 -#: netbox/dcim/forms/bulk_edit.py:1812 netbox/dcim/forms/bulk_import.py:91 +#: netbox/dcim/forms/bulk_edit.py:1819 netbox/dcim/forms/bulk_import.py:91 #: netbox/dcim/forms/bulk_import.py:150 netbox/dcim/forms/bulk_import.py:254 #: netbox/dcim/forms/bulk_import.py:563 netbox/dcim/forms/bulk_import.py:717 #: netbox/dcim/forms/bulk_import.py:1168 netbox/dcim/forms/bulk_import.py:1375 @@ -770,7 +770,7 @@ msgstr "" #: netbox/dcim/forms/filtersets.py:1394 netbox/dcim/forms/filtersets.py:1645 #: netbox/dcim/tables/devices.py:150 netbox/dcim/tables/devices.py:524 #: netbox/dcim/tables/devices.py:855 netbox/dcim/tables/devices.py:989 -#: netbox/dcim/tables/devices.py:1101 netbox/dcim/tables/modules.py:104 +#: netbox/dcim/tables/devices.py:1100 netbox/dcim/tables/modules.py:104 #: netbox/dcim/tables/power.py:74 netbox/dcim/tables/racks.py:129 #: netbox/dcim/tables/sites.py:88 netbox/dcim/tables/sites.py:143 #: netbox/ipam/forms/bulk_edit.py:240 netbox/ipam/forms/bulk_edit.py:290 @@ -836,7 +836,7 @@ msgstr "" #: netbox/circuits/forms/filtersets.py:332 netbox/dcim/forms/bulk_edit.py:131 #: netbox/dcim/forms/bulk_edit.py:196 netbox/dcim/forms/bulk_edit.py:361 #: netbox/dcim/forms/bulk_edit.py:484 netbox/dcim/forms/bulk_edit.py:743 -#: netbox/dcim/forms/bulk_edit.py:856 netbox/dcim/forms/bulk_edit.py:1817 +#: netbox/dcim/forms/bulk_edit.py:856 netbox/dcim/forms/bulk_edit.py:1824 #: netbox/dcim/forms/bulk_import.py:110 netbox/dcim/forms/bulk_import.py:155 #: netbox/dcim/forms/bulk_import.py:247 netbox/dcim/forms/bulk_import.py:362 #: netbox/dcim/forms/bulk_import.py:537 netbox/dcim/forms/bulk_import.py:1387 @@ -1211,7 +1211,7 @@ msgstr "" #: netbox/dcim/forms/bulk_import.py:1268 netbox/dcim/forms/model_forms.py:1289 #: netbox/dcim/forms/model_forms.py:1558 netbox/dcim/forms/model_forms.py:1725 #: netbox/dcim/forms/model_forms.py:1760 netbox/dcim/forms/model_forms.py:1890 -#: netbox/dcim/tables/connections.py:65 netbox/dcim/tables/devices.py:1147 +#: netbox/dcim/tables/connections.py:65 netbox/dcim/tables/devices.py:1146 #: netbox/ipam/forms/bulk_import.py:324 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:330 netbox/ipam/tables/vlans.py:147 @@ -1508,7 +1508,7 @@ msgid "member ID" msgstr "" #: netbox/circuits/models/circuits.py:201 netbox/ipam/models/fhrp.py:96 -#: netbox/tenancy/models/contacts.py:133 +#: netbox/tenancy/models/contacts.py:119 msgid "priority" msgstr "" @@ -1606,7 +1606,7 @@ msgstr "" #: netbox/ipam/models/vlans.py:206 netbox/ipam/models/vlans.py:352 #: netbox/ipam/models/vrfs.py:20 netbox/ipam/models/vrfs.py:75 #: netbox/netbox/models/__init__.py:142 netbox/netbox/models/__init__.py:190 -#: netbox/tenancy/models/contacts.py:59 netbox/tenancy/models/tenants.py:19 +#: netbox/tenancy/models/contacts.py:57 netbox/tenancy/models/tenants.py:19 #: netbox/tenancy/models/tenants.py:42 netbox/users/models/permissions.py:19 #: netbox/users/models/users.py:28 netbox/virtualization/models/clusters.py:52 #: netbox/virtualization/models/virtualmachines.py:71 @@ -1939,7 +1939,7 @@ msgstr "" #: netbox/circuits/tables/virtual_circuits.py:109 #: netbox/dcim/forms/bulk_edit.py:789 netbox/dcim/forms/bulk_edit.py:1343 -#: netbox/dcim/forms/bulk_edit.py:1755 netbox/dcim/forms/bulk_edit.py:1807 +#: netbox/dcim/forms/bulk_edit.py:1755 netbox/dcim/forms/bulk_edit.py:1814 #: netbox/dcim/forms/bulk_import.py:699 netbox/dcim/forms/bulk_import.py:761 #: netbox/dcim/forms/bulk_import.py:787 netbox/dcim/forms/bulk_import.py:813 #: netbox/dcim/forms/bulk_import.py:833 netbox/dcim/forms/bulk_import.py:889 @@ -3088,7 +3088,7 @@ msgstr "" #: netbox/dcim/forms/model_forms.py:517 netbox/dcim/forms/model_forms.py:1207 #: netbox/dcim/forms/model_forms.py:1676 netbox/dcim/forms/object_import.py:177 #: netbox/dcim/tables/devices.py:696 netbox/dcim/tables/devices.py:906 -#: netbox/dcim/tables/devices.py:993 netbox/dcim/tables/devices.py:1153 +#: netbox/dcim/tables/devices.py:993 netbox/dcim/tables/devices.py:1152 #: netbox/extras/tables/tables.py:237 netbox/ipam/forms/bulk_import.py:568 #: netbox/ipam/forms/model_forms.py:768 netbox/ipam/tables/fhrp.py:59 #: netbox/ipam/tables/ip.py:336 netbox/ipam/tables/services.py:44 @@ -3884,7 +3884,7 @@ msgid "LAG interface (ID)" msgstr "" #: netbox/dcim/filtersets.py:1923 netbox/dcim/tables/devices.py:612 -#: netbox/dcim/tables/devices.py:1142 netbox/templates/dcim/interface.html:131 +#: netbox/dcim/tables/devices.py:1141 netbox/templates/dcim/interface.html:131 #: netbox/templates/dcim/macaddress.html:11 #: netbox/templates/dcim/macaddress.html:14 #: netbox/templates/virtualization/vminterface.html:79 @@ -5341,12 +5341,12 @@ msgstr "" 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/dcim/forms/mixins.py:27 netbox/dcim/forms/mixins.py:79 #: netbox/ipam/forms/bulk_edit.py:425 netbox/ipam/forms/model_forms.py:617 msgid "Scope type" msgstr "" -#: netbox/dcim/forms/mixins.py:30 netbox/dcim/forms/mixins.py:78 +#: netbox/dcim/forms/mixins.py:30 netbox/dcim/forms/mixins.py:82 #: netbox/ipam/forms/bulk_edit.py:270 netbox/ipam/forms/bulk_edit.py:428 #: netbox/ipam/forms/bulk_edit.py:447 netbox/ipam/forms/filtersets.py:181 #: netbox/ipam/forms/model_forms.py:231 netbox/ipam/forms/model_forms.py:620 @@ -5365,7 +5365,7 @@ msgstr "" msgid "Scope" msgstr "" -#: netbox/dcim/forms/mixins.py:104 netbox/ipam/forms/bulk_import.py:452 +#: netbox/dcim/forms/mixins.py:108 netbox/ipam/forms/bulk_import.py:452 msgid "Scope type (app & model)" msgstr "" @@ -7244,7 +7244,7 @@ msgstr "" msgid "Config Template" msgstr "" -#: netbox/dcim/tables/devices.py:197 netbox/dcim/tables/devices.py:1106 +#: netbox/dcim/tables/devices.py:197 netbox/dcim/tables/devices.py:1105 #: netbox/ipam/forms/bulk_import.py:587 netbox/ipam/forms/model_forms.py:316 #: netbox/ipam/forms/model_forms.py:329 netbox/ipam/tables/ip.py:314 #: netbox/ipam/tables/ip.py:381 netbox/ipam/tables/ip.py:391 @@ -7253,12 +7253,12 @@ msgstr "" msgid "IP Address" msgstr "" -#: netbox/dcim/tables/devices.py:201 netbox/dcim/tables/devices.py:1110 +#: netbox/dcim/tables/devices.py:201 netbox/dcim/tables/devices.py:1109 #: netbox/virtualization/tables/virtualmachines.py:56 msgid "IPv4 Address" msgstr "" -#: netbox/dcim/tables/devices.py:205 netbox/dcim/tables/devices.py:1114 +#: netbox/dcim/tables/devices.py:205 netbox/dcim/tables/devices.py:1113 #: netbox/virtualization/tables/virtualmachines.py:60 msgid "IPv6 Address" msgstr "" @@ -7296,7 +7296,7 @@ msgstr "" msgid "Power outlets" msgstr "" -#: netbox/dcim/tables/devices.py:256 netbox/dcim/tables/devices.py:1119 +#: netbox/dcim/tables/devices.py:256 netbox/dcim/tables/devices.py:1118 #: netbox/dcim/tables/devicetypes.py:133 netbox/dcim/views.py:1173 #: netbox/dcim/views.py:1473 netbox/dcim/views.py:2226 #: netbox/netbox/navigation/menu.py:95 netbox/netbox/navigation/menu.py:259 @@ -10645,7 +10645,7 @@ msgstr "" msgid "Defined range exceeds maximum supported size ({max_size})" msgstr "" -#: netbox/ipam/models/ip.py:721 netbox/tenancy/models/contacts.py:78 +#: netbox/ipam/models/ip.py:721 netbox/tenancy/models/contacts.py:76 msgid "address" msgstr "" @@ -12043,6 +12043,7 @@ msgid "Toggle all" msgstr "" #: netbox/netbox/tables/columns.py:307 +#: netbox/templates/inc/table_controls_htmx.html:35 msgid "Toggle Dropdown" msgstr "" @@ -15189,63 +15190,55 @@ msgstr "" msgid "Assigned contact" msgstr "" -#: netbox/tenancy/models/contacts.py:33 +#: netbox/tenancy/models/contacts.py:32 msgid "contact group" msgstr "" -#: netbox/tenancy/models/contacts.py:34 +#: netbox/tenancy/models/contacts.py:33 msgid "contact groups" msgstr "" -#: netbox/tenancy/models/contacts.py:43 +#: netbox/tenancy/models/contacts.py:42 msgid "contact role" msgstr "" -#: netbox/tenancy/models/contacts.py:44 +#: netbox/tenancy/models/contacts.py:43 msgid "contact roles" msgstr "" -#: netbox/tenancy/models/contacts.py:64 +#: netbox/tenancy/models/contacts.py:62 msgid "title" msgstr "" -#: netbox/tenancy/models/contacts.py:69 +#: netbox/tenancy/models/contacts.py:67 msgid "phone" msgstr "" -#: netbox/tenancy/models/contacts.py:74 +#: netbox/tenancy/models/contacts.py:72 msgid "email" msgstr "" -#: netbox/tenancy/models/contacts.py:83 +#: netbox/tenancy/models/contacts.py:81 msgid "link" msgstr "" -#: netbox/tenancy/models/contacts.py:93 +#: netbox/tenancy/models/contacts.py:91 msgid "contact" msgstr "" -#: netbox/tenancy/models/contacts.py:94 +#: netbox/tenancy/models/contacts.py:92 msgid "contacts" msgstr "" -#: netbox/tenancy/models/contacts.py:108 -msgid "contact group membership" -msgstr "" - -#: netbox/tenancy/models/contacts.py:109 -msgid "contact group memberships" -msgstr "" - -#: netbox/tenancy/models/contacts.py:153 +#: netbox/tenancy/models/contacts.py:139 msgid "contact assignment" msgstr "" -#: netbox/tenancy/models/contacts.py:154 +#: netbox/tenancy/models/contacts.py:140 msgid "contact assignments" msgstr "" -#: netbox/tenancy/models/contacts.py:170 +#: netbox/tenancy/models/contacts.py:156 #, python-brace-format msgid "Contacts cannot be assigned to this object type ({type})." msgstr "" From 39b03abe721c9fe6aca092a6807e285a2babcf28 Mon Sep 17 00:00:00 2001 From: Aaron Queen Date: Tue, 13 May 2025 20:26:01 -0400 Subject: [PATCH 03/30] Use colored labels when displaying virtual circuit types --- netbox/circuits/tables/virtual_circuits.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/netbox/circuits/tables/virtual_circuits.py b/netbox/circuits/tables/virtual_circuits.py index 67ac03d59..ea3b6dc13 100644 --- a/netbox/circuits/tables/virtual_circuits.py +++ b/netbox/circuits/tables/virtual_circuits.py @@ -54,9 +54,8 @@ class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable) linkify=True, verbose_name=_('Account') ) - type = tables.Column( + type = columns.ColoredLabelColumn( verbose_name=_('Type'), - linkify=True ) status = columns.ChoiceFieldColumn() termination_count = columns.LinkedCountColumn( From 1700a9265c1b2a86f384590b379328788aea986c Mon Sep 17 00:00:00 2001 From: Renato Almeida de Oliveira Date: Wed, 14 May 2025 09:28:11 -0300 Subject: [PATCH 04/30] Closes: #19200 Add Virtual Chassis name to pane on Device View (#19369) --- netbox/templates/dcim/device.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 8f27a5cc1..5bd731535 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -26,6 +26,12 @@ {% trans "Location" %} {% nested_tree object.location %} + {% if object.virtual_chassis %} + + {% trans "Virtual Chassis" %} + {{ object.virtual_chassis|linkify }} + + {% endif %} {% trans "Rack" %} From cf7ab43f397172f71b2068eb5347953c0d408ce9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 14 May 2025 11:34:25 -0400 Subject: [PATCH 05/30] Closes #19493: Change filter() to filter_type() (#19494) --- netbox/circuits/graphql/filters.py | 22 +++--- netbox/core/graphql/filters.py | 8 +-- netbox/dcim/graphql/filters.py | 88 ++++++++++++------------ netbox/extras/graphql/filters.py | 28 ++++---- netbox/ipam/graphql/filters.py | 36 +++++----- netbox/tenancy/graphql/filters.py | 12 ++-- netbox/users/graphql/filters.py | 4 +- netbox/virtualization/graphql/filters.py | 12 ++-- netbox/vpn/graphql/filters.py | 20 +++--- netbox/wireless/graphql/filters.py | 6 +- 10 files changed, 118 insertions(+), 118 deletions(-) diff --git a/netbox/circuits/graphql/filters.py b/netbox/circuits/graphql/filters.py index 966849fd0..d6ef2976d 100644 --- a/netbox/circuits/graphql/filters.py +++ b/netbox/circuits/graphql/filters.py @@ -41,7 +41,7 @@ __all__ = ( ) -@strawberry_django.filter(models.CircuitTermination, lookups=True) +@strawberry_django.filter_type(models.CircuitTermination, lookups=True) class CircuitTerminationFilter( BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, @@ -87,7 +87,7 @@ class CircuitTerminationFilter( ) -@strawberry_django.filter(models.Circuit, lookups=True) +@strawberry_django.filter_type(models.Circuit, lookups=True) class CircuitFilter( ContactFilterMixin, ImageAttachmentFilterMixin, @@ -121,17 +121,17 @@ class CircuitFilter( ) -@strawberry_django.filter(models.CircuitType, lookups=True) +@strawberry_django.filter_type(models.CircuitType, lookups=True) class CircuitTypeFilter(BaseCircuitTypeFilterMixin): pass -@strawberry_django.filter(models.CircuitGroup, lookups=True) +@strawberry_django.filter_type(models.CircuitGroup, lookups=True) class CircuitGroupFilter(TenancyFilterMixin, OrganizationalModelFilterMixin): pass -@strawberry_django.filter(models.CircuitGroupAssignment, lookups=True) +@strawberry_django.filter_type(models.CircuitGroupAssignment, lookups=True) class CircuitGroupAssignmentFilter( BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin ): @@ -148,7 +148,7 @@ class CircuitGroupAssignmentFilter( ) -@strawberry_django.filter(models.Provider, lookups=True) +@strawberry_django.filter_type(models.Provider, lookups=True) class ProviderFilter(ContactFilterMixin, PrimaryModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() slug: FilterLookup[str] | None = strawberry_django.filter_field() @@ -158,7 +158,7 @@ class ProviderFilter(ContactFilterMixin, PrimaryModelFilterMixin): ) -@strawberry_django.filter(models.ProviderAccount, lookups=True) +@strawberry_django.filter_type(models.ProviderAccount, lookups=True) class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilterMixin): provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = ( strawberry_django.filter_field() @@ -168,7 +168,7 @@ class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.ProviderNetwork, lookups=True) +@strawberry_django.filter_type(models.ProviderNetwork, lookups=True) class ProviderNetworkFilter(PrimaryModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = ( @@ -178,12 +178,12 @@ class ProviderNetworkFilter(PrimaryModelFilterMixin): service_id: FilterLookup[str] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.VirtualCircuitType, lookups=True) +@strawberry_django.filter_type(models.VirtualCircuitType, lookups=True) class VirtualCircuitTypeFilter(BaseCircuitTypeFilterMixin): pass -@strawberry_django.filter(models.VirtualCircuit, lookups=True) +@strawberry_django.filter_type(models.VirtualCircuit, lookups=True) class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin): cid: FilterLookup[str] | None = strawberry_django.filter_field() provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = ( @@ -206,7 +206,7 @@ class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin): ) -@strawberry_django.filter(models.VirtualCircuitTermination, lookups=True) +@strawberry_django.filter_type(models.VirtualCircuitTermination, lookups=True) class VirtualCircuitTerminationFilter( BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin ): diff --git a/netbox/core/graphql/filters.py b/netbox/core/graphql/filters.py index e5d44674a..76ace2362 100644 --- a/netbox/core/graphql/filters.py +++ b/netbox/core/graphql/filters.py @@ -23,7 +23,7 @@ __all__ = ( ) -@strawberry_django.filter(models.DataFile, lookups=True) +@strawberry_django.filter_type(models.DataFile, lookups=True) class DataFileFilter(BaseFilterMixin): id: ID | None = strawberry_django.filter_field() created: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field() @@ -39,7 +39,7 @@ class DataFileFilter(BaseFilterMixin): hash: FilterLookup[str] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.DataSource, lookups=True) +@strawberry_django.filter_type(models.DataSource, lookups=True) class DataSourceFilter(PrimaryModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() type: FilterLookup[str] | None = strawberry_django.filter_field() @@ -56,7 +56,7 @@ class DataSourceFilter(PrimaryModelFilterMixin): ) -@strawberry_django.filter(models.ObjectChange, lookups=True) +@strawberry_django.filter_type(models.ObjectChange, lookups=True) class ObjectChangeFilter(BaseFilterMixin): id: ID | None = strawberry_django.filter_field() time: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field() @@ -82,7 +82,7 @@ class ObjectChangeFilter(BaseFilterMixin): ) -@strawberry_django.filter(DjangoContentType, lookups=True) +@strawberry_django.filter_type(DjangoContentType, lookups=True) class ContentTypeFilter(BaseFilterMixin): id: ID | None = strawberry_django.filter_field() app_label: FilterLookup[str] | None = strawberry_django.filter_field() diff --git a/netbox/dcim/graphql/filters.py b/netbox/dcim/graphql/filters.py index 77e7a53b9..a8a6c2a5e 100644 --- a/netbox/dcim/graphql/filters.py +++ b/netbox/dcim/graphql/filters.py @@ -90,7 +90,7 @@ __all__ = ( ) -@strawberry_django.filter(models.Cable, lookups=True) +@strawberry_django.filter_type(models.Cable, lookups=True) class CableFilter(PrimaryModelFilterMixin, TenancyFilterMixin): type: Annotated['CableTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field() status: Annotated['LinkStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field() @@ -107,7 +107,7 @@ class CableFilter(PrimaryModelFilterMixin, TenancyFilterMixin): ) -@strawberry_django.filter(models.CableTermination, lookups=True) +@strawberry_django.filter_type(models.CableTermination, lookups=True) class CableTerminationFilter(ChangeLogFilterMixin): cable: Annotated['CableFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() cable_id: ID | None = strawberry_django.filter_field() @@ -120,7 +120,7 @@ class CableTerminationFilter(ChangeLogFilterMixin): termination_id: ID | None = strawberry_django.filter_field() -@strawberry_django.filter(models.ConsolePort, lookups=True) +@strawberry_django.filter_type(models.ConsolePort, lookups=True) class ConsolePortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin): type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( strawberry_django.filter_field() @@ -130,14 +130,14 @@ class ConsolePortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilte ) -@strawberry_django.filter(models.ConsolePortTemplate, lookups=True) +@strawberry_django.filter_type(models.ConsolePortTemplate, lookups=True) class ConsolePortTemplateFilter(ModularComponentTemplateFilterMixin): type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( strawberry_django.filter_field() ) -@strawberry_django.filter(models.ConsoleServerPort, lookups=True) +@strawberry_django.filter_type(models.ConsoleServerPort, lookups=True) class ConsoleServerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin): type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( strawberry_django.filter_field() @@ -147,14 +147,14 @@ class ConsoleServerPortFilter(ModularComponentModelFilterMixin, CabledObjectMode ) -@strawberry_django.filter(models.ConsoleServerPortTemplate, lookups=True) +@strawberry_django.filter_type(models.ConsoleServerPortTemplate, lookups=True) class ConsoleServerPortTemplateFilter(ModularComponentTemplateFilterMixin): type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( strawberry_django.filter_field() ) -@strawberry_django.filter(models.Device, lookups=True) +@strawberry_django.filter_type(models.Device, lookups=True) class DeviceFilter( ContactFilterMixin, TenancyFilterMixin, @@ -271,7 +271,7 @@ class DeviceFilter( inventory_item_count: FilterLookup[int] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.DeviceBay, lookups=True) +@strawberry_django.filter_type(models.DeviceBay, lookups=True) class DeviceBayFilter(ComponentModelFilterMixin): installed_device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( strawberry_django.filter_field() @@ -279,12 +279,12 @@ class DeviceBayFilter(ComponentModelFilterMixin): installed_device_id: ID | None = strawberry_django.filter_field() -@strawberry_django.filter(models.DeviceBayTemplate, lookups=True) +@strawberry_django.filter_type(models.DeviceBayTemplate, lookups=True) class DeviceBayTemplateFilter(ComponentTemplateFilterMixin): pass -@strawberry_django.filter(models.InventoryItemTemplate, lookups=True) +@strawberry_django.filter_type(models.InventoryItemTemplate, lookups=True) class InventoryItemTemplateFilter(ComponentTemplateFilterMixin): parent: Annotated['InventoryItemTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( strawberry_django.filter_field() @@ -304,13 +304,13 @@ class InventoryItemTemplateFilter(ComponentTemplateFilterMixin): part_id: FilterLookup[str] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.DeviceRole, lookups=True) +@strawberry_django.filter_type(models.DeviceRole, lookups=True) class DeviceRoleFilter(OrganizationalModelFilterMixin, RenderConfigFilterMixin): color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() vm_role: FilterLookup[bool] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.DeviceType, lookups=True) +@strawberry_django.filter_type(models.DeviceType, lookups=True) class DeviceTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin): manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( strawberry_django.filter_field() @@ -382,7 +382,7 @@ class DeviceTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, Weig inventory_item_template_count: FilterLookup[int] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.FrontPort, lookups=True) +@strawberry_django.filter_type(models.FrontPort, lookups=True) class FrontPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin): type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field() color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() @@ -395,7 +395,7 @@ class FrontPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterM ) -@strawberry_django.filter(models.FrontPortTemplate, lookups=True) +@strawberry_django.filter_type(models.FrontPortTemplate, lookups=True) class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin): type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field() color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() @@ -408,7 +408,7 @@ class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin): ) -@strawberry_django.filter(models.MACAddress, lookups=True) +@strawberry_django.filter_type(models.MACAddress, lookups=True) class MACAddressFilter(PrimaryModelFilterMixin): mac_address: FilterLookup[str] | None = strawberry_django.filter_field() assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( @@ -417,7 +417,7 @@ class MACAddressFilter(PrimaryModelFilterMixin): assigned_object_id: ID | None = strawberry_django.filter_field() -@strawberry_django.filter(models.Interface, lookups=True) +@strawberry_django.filter_type(models.Interface, lookups=True) class InterfaceFilter(ModularComponentModelFilterMixin, InterfaceBaseFilterMixin, CabledObjectModelFilterMixin): vcdcs: Annotated['VirtualDeviceContextFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( strawberry_django.filter_field() @@ -486,7 +486,7 @@ class InterfaceFilter(ModularComponentModelFilterMixin, InterfaceBaseFilterMixin ) -@strawberry_django.filter(models.InterfaceTemplate, lookups=True) +@strawberry_django.filter_type(models.InterfaceTemplate, lookups=True) class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin): type: Annotated['InterfaceTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( strawberry_django.filter_field() @@ -508,7 +508,7 @@ class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin): ) -@strawberry_django.filter(models.InventoryItem, lookups=True) +@strawberry_django.filter_type(models.InventoryItem, lookups=True) class InventoryItemFilter(ComponentModelFilterMixin): parent: Annotated['InventoryItemFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( strawberry_django.filter_field() @@ -535,12 +535,12 @@ class InventoryItemFilter(ComponentModelFilterMixin): discovered: FilterLookup[bool] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.InventoryItemRole, lookups=True) +@strawberry_django.filter_type(models.InventoryItemRole, lookups=True) class InventoryItemRoleFilter(OrganizationalModelFilterMixin): color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.Location, lookups=True) +@strawberry_django.filter_type(models.Location, lookups=True) class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, NestedGroupModelFilterMixin): site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() site_id: ID | None = strawberry_django.filter_field() @@ -556,12 +556,12 @@ class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilt ) -@strawberry_django.filter(models.Manufacturer, lookups=True) +@strawberry_django.filter_type(models.Manufacturer, lookups=True) class ManufacturerFilter(ContactFilterMixin, OrganizationalModelFilterMixin): pass -@strawberry_django.filter(models.Module, lookups=True) +@strawberry_django.filter_type(models.Module, lookups=True) class ModuleFilter(PrimaryModelFilterMixin, ConfigContextFilterMixin): device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() device_id: ID | None = strawberry_django.filter_field() @@ -610,7 +610,7 @@ class ModuleFilter(PrimaryModelFilterMixin, ConfigContextFilterMixin): ) -@strawberry_django.filter(models.ModuleBay, lookups=True) +@strawberry_django.filter_type(models.ModuleBay, lookups=True) class ModuleBayFilter(ModularComponentModelFilterMixin): parent: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( strawberry_django.filter_field() @@ -619,17 +619,17 @@ class ModuleBayFilter(ModularComponentModelFilterMixin): position: FilterLookup[str] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.ModuleBayTemplate, lookups=True) +@strawberry_django.filter_type(models.ModuleBayTemplate, lookups=True) class ModuleBayTemplateFilter(ModularComponentTemplateFilterMixin): position: FilterLookup[str] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.ModuleTypeProfile, lookups=True) +@strawberry_django.filter_type(models.ModuleTypeProfile, lookups=True) class ModuleTypeProfileFilter(PrimaryModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.ModuleType, lookups=True) +@strawberry_django.filter_type(models.ModuleType, lookups=True) class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin): manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( strawberry_django.filter_field() @@ -676,7 +676,7 @@ class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, Weig ) = strawberry_django.filter_field() -@strawberry_django.filter(models.Platform, lookups=True) +@strawberry_django.filter_type(models.Platform, lookups=True) class PlatformFilter(OrganizationalModelFilterMixin): manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( strawberry_django.filter_field() @@ -688,7 +688,7 @@ class PlatformFilter(OrganizationalModelFilterMixin): config_template_id: ID | None = strawberry_django.filter_field() -@strawberry_django.filter(models.PowerFeed, lookups=True) +@strawberry_django.filter_type(models.PowerFeed, lookups=True) class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin): power_panel: Annotated['PowerPanelFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( strawberry_django.filter_field() @@ -723,7 +723,7 @@ class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryM ) -@strawberry_django.filter(models.PowerOutlet, lookups=True) +@strawberry_django.filter_type(models.PowerOutlet, lookups=True) class PowerOutletFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin): type: Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( strawberry_django.filter_field() @@ -738,7 +738,7 @@ class PowerOutletFilter(ModularComponentModelFilterMixin, CabledObjectModelFilte color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.PowerOutletTemplate, lookups=True) +@strawberry_django.filter_type(models.PowerOutletTemplate, lookups=True) class PowerOutletTemplateFilter(ModularComponentModelFilterMixin): type: Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( strawberry_django.filter_field() @@ -752,7 +752,7 @@ class PowerOutletTemplateFilter(ModularComponentModelFilterMixin): ) -@strawberry_django.filter(models.PowerPanel, lookups=True) +@strawberry_django.filter_type(models.PowerPanel, lookups=True) class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryModelFilterMixin): site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() site_id: ID | None = strawberry_django.filter_field() @@ -765,7 +765,7 @@ class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryMo name: FilterLookup[str] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.PowerPort, lookups=True) +@strawberry_django.filter_type(models.PowerPort, lookups=True) class PowerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin): type: Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( strawberry_django.filter_field() @@ -778,7 +778,7 @@ class PowerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterM ) -@strawberry_django.filter(models.PowerPortTemplate, lookups=True) +@strawberry_django.filter_type(models.PowerPortTemplate, lookups=True) class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin): type: Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( strawberry_django.filter_field() @@ -791,7 +791,7 @@ class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin): ) -@strawberry_django.filter(models.RackType, lookups=True) +@strawberry_django.filter_type(models.RackType, lookups=True) class RackTypeFilter(RackBaseFilterMixin): form_factor: Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( strawberry_django.filter_field() @@ -804,7 +804,7 @@ class RackTypeFilter(RackBaseFilterMixin): slug: FilterLookup[str] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.Rack, lookups=True) +@strawberry_django.filter_type(models.Rack, lookups=True) class RackFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, RackBaseFilterMixin): form_factor: Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( strawberry_django.filter_field() @@ -836,7 +836,7 @@ class RackFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMi ) -@strawberry_django.filter(models.RackReservation, lookups=True) +@strawberry_django.filter_type(models.RackReservation, lookups=True) class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilterMixin): rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() rack_id: ID | None = strawberry_django.filter_field() @@ -848,12 +848,12 @@ class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilterMixin): description: FilterLookup[str] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.RackRole, lookups=True) +@strawberry_django.filter_type(models.RackRole, lookups=True) class RackRoleFilter(OrganizationalModelFilterMixin): color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.RearPort, lookups=True) +@strawberry_django.filter_type(models.RearPort, lookups=True) class RearPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin): type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field() color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() @@ -862,7 +862,7 @@ class RearPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMi ) -@strawberry_django.filter(models.RearPortTemplate, lookups=True) +@strawberry_django.filter_type(models.RearPortTemplate, lookups=True) class RearPortTemplateFilter(ModularComponentTemplateFilterMixin): type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field() color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() @@ -871,7 +871,7 @@ class RearPortTemplateFilter(ModularComponentTemplateFilterMixin): ) -@strawberry_django.filter(models.Region, lookups=True) +@strawberry_django.filter_type(models.Region, lookups=True) class RegionFilter(ContactFilterMixin, NestedGroupModelFilterMixin): prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( strawberry_django.filter_field() @@ -881,7 +881,7 @@ class RegionFilter(ContactFilterMixin, NestedGroupModelFilterMixin): ) -@strawberry_django.filter(models.Site, lookups=True) +@strawberry_django.filter_type(models.Site, lookups=True) class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() slug: FilterLookup[str] | None = strawberry_django.filter_field() @@ -915,7 +915,7 @@ class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMi ) -@strawberry_django.filter(models.SiteGroup, lookups=True) +@strawberry_django.filter_type(models.SiteGroup, lookups=True) class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilterMixin): prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( strawberry_django.filter_field() @@ -925,7 +925,7 @@ class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilterMixin): ) -@strawberry_django.filter(models.VirtualChassis, lookups=True) +@strawberry_django.filter_type(models.VirtualChassis, lookups=True) class VirtualChassisFilter(PrimaryModelFilterMixin): master: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() master_id: ID | None = strawberry_django.filter_field() @@ -937,7 +937,7 @@ class VirtualChassisFilter(PrimaryModelFilterMixin): member_count: FilterLookup[int] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.VirtualDeviceContext, lookups=True) +@strawberry_django.filter_type(models.VirtualDeviceContext, lookups=True) class VirtualDeviceContextFilter(TenancyFilterMixin, PrimaryModelFilterMixin): device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() device_id: ID | None = strawberry_django.filter_field() diff --git a/netbox/extras/graphql/filters.py b/netbox/extras/graphql/filters.py index 2798c4896..1712b7056 100644 --- a/netbox/extras/graphql/filters.py +++ b/netbox/extras/graphql/filters.py @@ -40,7 +40,7 @@ __all__ = ( ) -@strawberry_django.filter(models.ConfigContext, lookups=True) +@strawberry_django.filter_type(models.ConfigContext, lookups=True) class ConfigContextFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin): name: FilterLookup[str] = strawberry_django.filter_field() weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( @@ -97,7 +97,7 @@ class ConfigContextFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, Chan ) -@strawberry_django.filter(models.ConfigTemplate, lookups=True) +@strawberry_django.filter_type(models.ConfigTemplate, lookups=True) class ConfigTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() description: FilterLookup[str] | None = strawberry_django.filter_field() @@ -111,7 +111,7 @@ class ConfigTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, Cha as_attachment: FilterLookup[bool] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.CustomField, lookups=True) +@strawberry_django.filter_type(models.CustomField, lookups=True) class CustomFieldFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): type: Annotated['CustomFieldTypeEnum', strawberry.lazy('extras.graphql.enums')] | None = ( strawberry_django.filter_field() @@ -164,7 +164,7 @@ class CustomFieldFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): comments: FilterLookup[str] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.CustomFieldChoiceSet, lookups=True) +@strawberry_django.filter_type(models.CustomFieldChoiceSet, lookups=True) class CustomFieldChoiceSetFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() description: FilterLookup[str] | None = strawberry_django.filter_field() @@ -177,7 +177,7 @@ class CustomFieldChoiceSetFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin order_alphabetically: FilterLookup[bool] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.CustomLink, lookups=True) +@strawberry_django.filter_type(models.CustomLink, lookups=True) class CustomLinkFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() enabled: FilterLookup[bool] | None = strawberry_django.filter_field() @@ -193,7 +193,7 @@ class CustomLinkFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): new_window: FilterLookup[bool] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.ExportTemplate, lookups=True) +@strawberry_django.filter_type(models.ExportTemplate, lookups=True) class ExportTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() description: FilterLookup[str] | None = strawberry_django.filter_field() @@ -207,7 +207,7 @@ class ExportTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, Cha as_attachment: FilterLookup[bool] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.ImageAttachment, lookups=True) +@strawberry_django.filter_type(models.ImageAttachment, lookups=True) class ImageAttachmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( strawberry_django.filter_field() @@ -222,7 +222,7 @@ class ImageAttachmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.JournalEntry, lookups=True) +@strawberry_django.filter_type(models.JournalEntry, lookups=True) class JournalEntryFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin): assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( strawberry_django.filter_field() @@ -238,7 +238,7 @@ class JournalEntryFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, Tag comments: FilterLookup[str] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.NotificationGroup, lookups=True) +@strawberry_django.filter_type(models.NotificationGroup, lookups=True) class NotificationGroupFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() description: FilterLookup[str] | None = strawberry_django.filter_field() @@ -246,7 +246,7 @@ class NotificationGroupFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): users: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.SavedFilter, lookups=True) +@strawberry_django.filter_type(models.SavedFilter, lookups=True) class SavedFilterFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() slug: FilterLookup[str] | None = strawberry_django.filter_field() @@ -263,7 +263,7 @@ class SavedFilterFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): ) -@strawberry_django.filter(models.TableConfig, lookups=True) +@strawberry_django.filter_type(models.TableConfig, lookups=True) class TableConfigFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() description: FilterLookup[str] | None = strawberry_django.filter_field() @@ -276,13 +276,13 @@ class TableConfigFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): shared: FilterLookup[bool] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.Tag, lookups=True) +@strawberry_django.filter_type(models.Tag, lookups=True) class TagFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin, TagBaseFilterMixin): color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() description: FilterLookup[str] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.Webhook, lookups=True) +@strawberry_django.filter_type(models.Webhook, lookups=True) class WebhookFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() description: FilterLookup[str] | None = strawberry_django.filter_field() @@ -301,7 +301,7 @@ class WebhookFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilt ) -@strawberry_django.filter(models.EventRule, lookups=True) +@strawberry_django.filter_type(models.EventRule, lookups=True) class EventRuleFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() description: FilterLookup[str] | None = strawberry_django.filter_field() diff --git a/netbox/ipam/graphql/filters.py b/netbox/ipam/graphql/filters.py index 9d168f3d9..511d4d53c 100644 --- a/netbox/ipam/graphql/filters.py +++ b/netbox/ipam/graphql/filters.py @@ -46,7 +46,7 @@ __all__ = ( ) -@strawberry_django.filter(models.ASN, lookups=True) +@strawberry_django.filter_type(models.ASN, lookups=True) class ASNFilter(TenancyFilterMixin, PrimaryModelFilterMixin): rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() rir_id: ID | None = strawberry_django.filter_field() @@ -61,7 +61,7 @@ class ASNFilter(TenancyFilterMixin, PrimaryModelFilterMixin): ) = strawberry_django.filter_field() -@strawberry_django.filter(models.ASNRange, lookups=True) +@strawberry_django.filter_type(models.ASNRange, lookups=True) class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() slug: FilterLookup[str] | None = strawberry_django.filter_field() @@ -75,7 +75,7 @@ class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilterMixin): ) -@strawberry_django.filter(models.Aggregate, lookups=True) +@strawberry_django.filter_type(models.Aggregate, lookups=True) class AggregateFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin): prefix: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() prefix_id: ID | None = strawberry_django.filter_field() @@ -84,7 +84,7 @@ class AggregateFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter date_added: DateFilterLookup[date] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.FHRPGroup, lookups=True) +@strawberry_django.filter_type(models.FHRPGroup, lookups=True) class FHRPGroupFilter(PrimaryModelFilterMixin): group_id: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( strawberry_django.filter_field() @@ -102,7 +102,7 @@ class FHRPGroupFilter(PrimaryModelFilterMixin): ) -@strawberry_django.filter(models.FHRPGroupAssignment, lookups=True) +@strawberry_django.filter_type(models.FHRPGroupAssignment, lookups=True) class FHRPGroupAssignmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): interface_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( strawberry_django.filter_field() @@ -117,7 +117,7 @@ class FHRPGroupAssignmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin) ) -@strawberry_django.filter(models.IPAddress, lookups=True) +@strawberry_django.filter_type(models.IPAddress, lookups=True) class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin): address: FilterLookup[str] | None = strawberry_django.filter_field() vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() @@ -156,7 +156,7 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter return q -@strawberry_django.filter(models.IPRange, lookups=True) +@strawberry_django.filter_type(models.IPRange, lookups=True) class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin): start_address: FilterLookup[str] | None = strawberry_django.filter_field() end_address: FilterLookup[str] | None = strawberry_django.filter_field() @@ -185,7 +185,7 @@ class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMi return q -@strawberry_django.filter(models.Prefix, lookups=True) +@strawberry_django.filter_type(models.Prefix, lookups=True) class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin): prefix: FilterLookup[str] | None = strawberry_django.filter_field() vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() @@ -201,19 +201,19 @@ class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, Pr mark_utilized: FilterLookup[bool] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.RIR, lookups=True) +@strawberry_django.filter_type(models.RIR, lookups=True) class RIRFilter(OrganizationalModelFilterMixin): is_private: FilterLookup[bool] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.Role, lookups=True) +@strawberry_django.filter_type(models.Role, lookups=True) class RoleFilter(OrganizationalModelFilterMixin): weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( strawberry_django.filter_field() ) -@strawberry_django.filter(models.RouteTarget, lookups=True) +@strawberry_django.filter_type(models.RouteTarget, lookups=True) class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() importing_vrfs: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( @@ -230,7 +230,7 @@ class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilterMixin): ) -@strawberry_django.filter(models.Service, lookups=True) +@strawberry_django.filter_type(models.Service, lookups=True) class ServiceFilter(ContactFilterMixin, ServiceBaseFilterMixin, PrimaryModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( @@ -242,12 +242,12 @@ class ServiceFilter(ContactFilterMixin, ServiceBaseFilterMixin, PrimaryModelFilt parent_object_id: ID | None = strawberry_django.filter_field() -@strawberry_django.filter(models.ServiceTemplate, lookups=True) +@strawberry_django.filter_type(models.ServiceTemplate, lookups=True) class ServiceTemplateFilter(ServiceBaseFilterMixin, PrimaryModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.VLAN, lookups=True) +@strawberry_django.filter_type(models.VLAN, lookups=True) class VLANFilter(TenancyFilterMixin, PrimaryModelFilterMixin): site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() site_id: ID | None = strawberry_django.filter_field() @@ -277,19 +277,19 @@ class VLANFilter(TenancyFilterMixin, PrimaryModelFilterMixin): ) -@strawberry_django.filter(models.VLANGroup, lookups=True) +@strawberry_django.filter_type(models.VLANGroup, lookups=True) class VLANGroupFilter(ScopedFilterMixin, OrganizationalModelFilterMixin): vid_ranges: Annotated['IntegerArrayLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( strawberry_django.filter_field() ) -@strawberry_django.filter(models.VLANTranslationPolicy, lookups=True) +@strawberry_django.filter_type(models.VLANTranslationPolicy, lookups=True) class VLANTranslationPolicyFilter(PrimaryModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.VLANTranslationRule, lookups=True) +@strawberry_django.filter_type(models.VLANTranslationRule, lookups=True) class VLANTranslationRuleFilter(NetBoxModelFilterMixin): policy: Annotated['VLANTranslationPolicyFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( strawberry_django.filter_field() @@ -304,7 +304,7 @@ class VLANTranslationRuleFilter(NetBoxModelFilterMixin): ) -@strawberry_django.filter(models.VRF, lookups=True) +@strawberry_django.filter_type(models.VRF, lookups=True) class VRFFilter(TenancyFilterMixin, PrimaryModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() rd: FilterLookup[str] | None = strawberry_django.filter_field() diff --git a/netbox/tenancy/graphql/filters.py b/netbox/tenancy/graphql/filters.py index f215fd8ab..fb37359ef 100644 --- a/netbox/tenancy/graphql/filters.py +++ b/netbox/tenancy/graphql/filters.py @@ -56,7 +56,7 @@ __all__ = ( ) -@strawberry_django.filter(models.Tenant, lookups=True) +@strawberry_django.filter_type(models.Tenant, lookups=True) class TenantFilter(PrimaryModelFilterMixin, ContactFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() slug: FilterLookup[str] | None = strawberry_django.filter_field() @@ -135,7 +135,7 @@ class TenantFilter(PrimaryModelFilterMixin, ContactFilterMixin): ) -@strawberry_django.filter(models.TenantGroup, lookups=True) +@strawberry_django.filter_type(models.TenantGroup, lookups=True) class TenantGroupFilter(OrganizationalModelFilterMixin): parent: Annotated['TenantGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = ( strawberry_django.filter_field() @@ -149,7 +149,7 @@ class TenantGroupFilter(OrganizationalModelFilterMixin): ) -@strawberry_django.filter(models.Contact, lookups=True) +@strawberry_django.filter_type(models.Contact, lookups=True) class ContactFilter(PrimaryModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() title: FilterLookup[str] | None = strawberry_django.filter_field() @@ -165,19 +165,19 @@ class ContactFilter(PrimaryModelFilterMixin): ) -@strawberry_django.filter(models.ContactRole, lookups=True) +@strawberry_django.filter_type(models.ContactRole, lookups=True) class ContactRoleFilter(OrganizationalModelFilterMixin): pass -@strawberry_django.filter(models.ContactGroup, lookups=True) +@strawberry_django.filter_type(models.ContactGroup, lookups=True) class ContactGroupFilter(NestedGroupModelFilterMixin): parent: Annotated['ContactGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = ( strawberry_django.filter_field() ) -@strawberry_django.filter(models.ContactAssignment, lookups=True) +@strawberry_django.filter_type(models.ContactAssignment, lookups=True) class ContactAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin): object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( strawberry_django.filter_field() diff --git a/netbox/users/graphql/filters.py b/netbox/users/graphql/filters.py index 8f8a8f946..60a80181b 100644 --- a/netbox/users/graphql/filters.py +++ b/netbox/users/graphql/filters.py @@ -14,13 +14,13 @@ __all__ = ( ) -@strawberry_django.filter(models.Group, lookups=True) +@strawberry_django.filter_type(models.Group, lookups=True) class GroupFilter(BaseObjectTypeFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() description: FilterLookup[str] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.User, lookups=True) +@strawberry_django.filter_type(models.User, lookups=True) class UserFilter(BaseObjectTypeFilterMixin): username: FilterLookup[str] | None = strawberry_django.filter_field() first_name: FilterLookup[str] | None = strawberry_django.filter_field() diff --git a/netbox/virtualization/graphql/filters.py b/netbox/virtualization/graphql/filters.py index 2a09e86d1..a10ade5a2 100644 --- a/netbox/virtualization/graphql/filters.py +++ b/netbox/virtualization/graphql/filters.py @@ -39,7 +39,7 @@ __all__ = ( ) -@strawberry_django.filter(models.Cluster, lookups=True) +@strawberry_django.filter_type(models.Cluster, lookups=True) class ClusterFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() type: Annotated['ClusterTypeFilter', strawberry.lazy('virtualization.graphql.filters')] | None = ( @@ -58,19 +58,19 @@ class ClusterFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, P ) -@strawberry_django.filter(models.ClusterGroup, lookups=True) +@strawberry_django.filter_type(models.ClusterGroup, lookups=True) class ClusterGroupFilter(ContactFilterMixin, OrganizationalModelFilterMixin): vlan_groups: Annotated['VLANGroupFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( strawberry_django.filter_field() ) -@strawberry_django.filter(models.ClusterType, lookups=True) +@strawberry_django.filter_type(models.ClusterType, lookups=True) class ClusterTypeFilter(OrganizationalModelFilterMixin): pass -@strawberry_django.filter(models.VirtualMachine, lookups=True) +@strawberry_django.filter_type(models.VirtualMachine, lookups=True) class VirtualMachineFilter( ContactFilterMixin, ImageAttachmentFilterMixin, @@ -130,7 +130,7 @@ class VirtualMachineFilter( ) -@strawberry_django.filter(models.VMInterface, lookups=True) +@strawberry_django.filter_type(models.VMInterface, lookups=True) class VMInterfaceFilter(VMComponentFilterMixin, InterfaceBaseFilterMixin): ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( strawberry_django.filter_field() @@ -155,7 +155,7 @@ class VMInterfaceFilter(VMComponentFilterMixin, InterfaceBaseFilterMixin): ) -@strawberry_django.filter(models.VirtualDisk, lookups=True) +@strawberry_django.filter_type(models.VirtualDisk, lookups=True) class VirtualDiskFilter(VMComponentFilterMixin): size: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( strawberry_django.filter_field() diff --git a/netbox/vpn/graphql/filters.py b/netbox/vpn/graphql/filters.py index f3ee290fe..21adcd100 100644 --- a/netbox/vpn/graphql/filters.py +++ b/netbox/vpn/graphql/filters.py @@ -31,12 +31,12 @@ __all__ = ( ) -@strawberry_django.filter(models.TunnelGroup, lookups=True) +@strawberry_django.filter_type(models.TunnelGroup, lookups=True) class TunnelGroupFilter(OrganizationalModelFilterMixin): pass -@strawberry_django.filter(models.TunnelTermination, lookups=True) +@strawberry_django.filter_type(models.TunnelTermination, lookups=True) class TunnelTerminationFilter( BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin ): @@ -56,7 +56,7 @@ class TunnelTerminationFilter( outside_ip_id: ID | None = strawberry_django.filter_field() -@strawberry_django.filter(models.Tunnel, lookups=True) +@strawberry_django.filter_type(models.Tunnel, lookups=True) class TunnelFilter(TenancyFilterMixin, PrimaryModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() status: Annotated['TunnelStatusEnum', strawberry.lazy('vpn.graphql.enums')] | None = ( @@ -80,7 +80,7 @@ class TunnelFilter(TenancyFilterMixin, PrimaryModelFilterMixin): ) -@strawberry_django.filter(models.IKEProposal, lookups=True) +@strawberry_django.filter_type(models.IKEProposal, lookups=True) class IKEProposalFilter(PrimaryModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() authentication_method: Annotated['AuthenticationMethodEnum', strawberry.lazy('vpn.graphql.enums')] | None = ( @@ -101,7 +101,7 @@ class IKEProposalFilter(PrimaryModelFilterMixin): ) -@strawberry_django.filter(models.IKEPolicy, lookups=True) +@strawberry_django.filter_type(models.IKEPolicy, lookups=True) class IKEPolicyFilter(PrimaryModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() version: Annotated['IKEVersionEnum', strawberry.lazy('vpn.graphql.enums')] | None = strawberry_django.filter_field() @@ -112,7 +112,7 @@ class IKEPolicyFilter(PrimaryModelFilterMixin): preshared_key: FilterLookup[str] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.IPSecProposal, lookups=True) +@strawberry_django.filter_type(models.IPSecProposal, lookups=True) class IPSecProposalFilter(PrimaryModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() encryption_algorithm: Annotated['EncryptionAlgorithmEnum', strawberry.lazy('vpn.graphql.enums')] | None = ( @@ -132,7 +132,7 @@ class IPSecProposalFilter(PrimaryModelFilterMixin): ) -@strawberry_django.filter(models.IPSecPolicy, lookups=True) +@strawberry_django.filter_type(models.IPSecPolicy, lookups=True) class IPSecPolicyFilter(PrimaryModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() proposals: Annotated['IPSecProposalFilter', strawberry.lazy('vpn.graphql.filters')] | None = ( @@ -141,7 +141,7 @@ class IPSecPolicyFilter(PrimaryModelFilterMixin): pfs_group: Annotated['DHGroupEnum', strawberry.lazy('vpn.graphql.enums')] | None = strawberry_django.filter_field() -@strawberry_django.filter(models.IPSecProfile, lookups=True) +@strawberry_django.filter_type(models.IPSecProfile, lookups=True) class IPSecProfileFilter(PrimaryModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() mode: Annotated['IPSecModeEnum', strawberry.lazy('vpn.graphql.enums')] | None = strawberry_django.filter_field() @@ -155,7 +155,7 @@ class IPSecProfileFilter(PrimaryModelFilterMixin): ipsec_policy_id: ID | None = strawberry_django.filter_field() -@strawberry_django.filter(models.L2VPN, lookups=True) +@strawberry_django.filter_type(models.L2VPN, lookups=True) class L2VPNFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() slug: FilterLookup[str] | None = strawberry_django.filter_field() @@ -174,7 +174,7 @@ class L2VPNFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixi ) -@strawberry_django.filter(models.L2VPNTermination, lookups=True) +@strawberry_django.filter_type(models.L2VPNTermination, lookups=True) class L2VPNTerminationFilter(NetBoxModelFilterMixin): l2vpn: Annotated['L2VPNFilter', strawberry.lazy('vpn.graphql.filters')] | None = strawberry_django.filter_field() l2vpn_id: ID | None = strawberry_django.filter_field() diff --git a/netbox/wireless/graphql/filters.py b/netbox/wireless/graphql/filters.py index d71af7ae2..5bd22afab 100644 --- a/netbox/wireless/graphql/filters.py +++ b/netbox/wireless/graphql/filters.py @@ -23,12 +23,12 @@ __all__ = ( ) -@strawberry_django.filter(models.WirelessLANGroup, lookups=True) +@strawberry_django.filter_type(models.WirelessLANGroup, lookups=True) class WirelessLANGroupFilter(NestedGroupModelFilterMixin): pass -@strawberry_django.filter(models.WirelessLAN, lookups=True) +@strawberry_django.filter_type(models.WirelessLAN, lookups=True) class WirelessLANFilter( WirelessAuthenticationBaseFilterMixin, ScopedFilterMixin, @@ -47,7 +47,7 @@ class WirelessLANFilter( vlan_id: ID | None = strawberry_django.filter_field() -@strawberry_django.filter(models.WirelessLink, lookups=True) +@strawberry_django.filter_type(models.WirelessLink, lookups=True) class WirelessLinkFilter( WirelessAuthenticationBaseFilterMixin, DistanceFilterMixin, From de2e2b5c8252c0ff5da208ef61762881c91c63a6 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 15 May 2025 05:02:17 +0000 Subject: [PATCH 06/30] Update source translation strings --- netbox/translations/en/LC_MESSAGES/django.po | 117 ++++++++++--------- 1 file changed, 59 insertions(+), 58 deletions(-) diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index a89bc7b78..0dee0d32f 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-05-14 05:01+0000\n" +"POT-Creation-Date: 2025-05-15 05:02+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -436,7 +436,7 @@ msgstr "" #: netbox/circuits/forms/filtersets.py:379 #: netbox/circuits/forms/model_forms.py:343 #: netbox/circuits/forms/model_forms.py:358 -#: netbox/circuits/tables/virtual_circuits.py:88 +#: netbox/circuits/tables/virtual_circuits.py:87 #: netbox/templates/circuits/virtualcircuit.html:20 #: netbox/templates/circuits/virtualcircuittermination.html:38 msgid "Virtual circuit" @@ -511,7 +511,8 @@ msgstr "" #: netbox/templates/core/plugin.html:80 netbox/templates/dcim/cable.html:36 #: netbox/templates/dcim/consoleport.html:44 #: netbox/templates/dcim/consoleserverport.html:44 -#: netbox/templates/dcim/device.html:94 netbox/templates/dcim/devicebay.html:32 +#: netbox/templates/dcim/device.html:100 +#: netbox/templates/dcim/devicebay.html:32 #: netbox/templates/dcim/devicerole.html:30 #: netbox/templates/dcim/devicetype.html:33 #: netbox/templates/dcim/frontport.html:58 @@ -627,7 +628,7 @@ msgstr "" #: 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/circuits/tables/virtual_circuits.py:92 #: netbox/templates/circuits/circuit.html:18 #: netbox/templates/circuits/circuitgroupassignment.html:26 #: netbox/templates/circuits/circuittermination.html:25 @@ -787,7 +788,7 @@ msgstr "" #: netbox/templates/circuits/virtualcircuit.html:43 #: netbox/templates/core/datasource.html:46 netbox/templates/core/job.html:48 #: netbox/templates/core/rq_task.html:81 netbox/templates/core/system.html:18 -#: netbox/templates/dcim/cable.html:19 netbox/templates/dcim/device.html:178 +#: netbox/templates/dcim/cable.html:19 netbox/templates/dcim/device.html:184 #: netbox/templates/dcim/inventoryitem.html:36 #: netbox/templates/dcim/location.html:45 netbox/templates/dcim/module.html:69 #: netbox/templates/dcim/powerfeed.html:36 @@ -866,7 +867,7 @@ msgstr "" #: netbox/ipam/tables/vlans.py:207 netbox/templates/circuits/circuit.html:48 #: netbox/templates/circuits/circuitgroup.html:36 #: netbox/templates/circuits/virtualcircuit.html:47 -#: netbox/templates/dcim/cable.html:23 netbox/templates/dcim/device.html:79 +#: netbox/templates/dcim/cable.html:23 netbox/templates/dcim/device.html:85 #: netbox/templates/dcim/location.html:49 #: netbox/templates/dcim/powerfeed.html:44 netbox/templates/dcim/rack.html:32 #: netbox/templates/dcim/rackreservation.html:49 @@ -1059,7 +1060,7 @@ msgstr "" #: netbox/circuits/forms/filtersets.py:305 #: netbox/circuits/tables/circuits.py:206 netbox/dcim/forms/model_forms.py:656 #: netbox/templates/circuits/circuitgroupassignment.html:34 -#: netbox/templates/dcim/device.html:133 +#: netbox/templates/dcim/device.html:139 #: netbox/templates/dcim/virtualchassis.html:68 #: netbox/templates/dcim/virtualchassis_edit.html:60 #: netbox/templates/ipam/inc/panels/fhrp_groups.html:26 @@ -1075,7 +1076,7 @@ msgstr "" #: netbox/circuits/forms/filtersets.py:392 #: netbox/circuits/forms/model_forms.py:325 #: netbox/circuits/tables/virtual_circuits.py:51 -#: netbox/circuits/tables/virtual_circuits.py:99 +#: netbox/circuits/tables/virtual_circuits.py:98 msgid "Provider network" msgstr "" @@ -1103,7 +1104,7 @@ msgstr "" #: netbox/ipam/tables/ip.py:269 netbox/ipam/tables/ip.py:325 #: netbox/ipam/tables/vlans.py:101 netbox/ipam/tables/vlans.py:213 #: netbox/templates/circuits/virtualcircuittermination.html:42 -#: netbox/templates/dcim/device.html:182 +#: netbox/templates/dcim/device.html:188 #: netbox/templates/dcim/inc/panels/inventory_items.html:20 #: netbox/templates/dcim/interface.html:178 #: netbox/templates/dcim/interface.html:280 @@ -1207,7 +1208,7 @@ msgstr "" #: netbox/circuits/forms/bulk_import.py:259 #: netbox/circuits/forms/model_forms.py:368 -#: netbox/circuits/tables/virtual_circuits.py:112 +#: netbox/circuits/tables/virtual_circuits.py:111 #: netbox/dcim/forms/bulk_import.py:1268 netbox/dcim/forms/model_forms.py:1289 #: netbox/dcim/forms/model_forms.py:1558 netbox/dcim/forms/model_forms.py:1725 #: netbox/dcim/forms/model_forms.py:1760 netbox/dcim/forms/model_forms.py:1890 @@ -1350,7 +1351,7 @@ msgstr "" #: netbox/circuits/forms/filtersets.py:82 netbox/circuits/tables/circuits.py:62 #: netbox/circuits/tables/providers.py:64 #: netbox/circuits/tables/virtual_circuits.py:55 -#: netbox/circuits/tables/virtual_circuits.py:103 +#: netbox/circuits/tables/virtual_circuits.py:102 #: netbox/templates/circuits/circuit.html:22 #: netbox/templates/circuits/provideraccount.html:24 msgid "Account" @@ -1856,7 +1857,7 @@ msgstr "" #: netbox/circuits/tables/circuits.py:83 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/circuits/tables/virtual_circuits.py:67 #: netbox/dcim/tables/devices.py:1074 netbox/dcim/tables/devicetypes.py:97 #: netbox/dcim/tables/modules.py:27 netbox/dcim/tables/modules.py:68 #: netbox/dcim/tables/modules.py:107 netbox/dcim/tables/power.py:39 @@ -1929,7 +1930,7 @@ msgstr "" msgid "ASN Count" msgstr "" -#: netbox/circuits/tables/virtual_circuits.py:65 +#: netbox/circuits/tables/virtual_circuits.py:64 #: netbox/netbox/navigation/menu.py:235 #: netbox/templates/circuits/virtualcircuit.html:87 #: netbox/templates/vpn/l2vpn.html:60 netbox/templates/vpn/tunnel.html:72 @@ -1937,7 +1938,7 @@ msgstr "" msgid "Terminations" msgstr "" -#: netbox/circuits/tables/virtual_circuits.py:109 +#: netbox/circuits/tables/virtual_circuits.py:108 #: netbox/dcim/forms/bulk_edit.py:789 netbox/dcim/forms/bulk_edit.py:1343 #: netbox/dcim/forms/bulk_edit.py:1755 netbox/dcim/forms/bulk_edit.py:1814 #: netbox/dcim/forms/bulk_import.py:699 netbox/dcim/forms/bulk_import.py:761 @@ -1972,7 +1973,7 @@ msgstr "" #: netbox/templates/circuits/virtualcircuittermination.html:56 #: netbox/templates/dcim/consoleport.html:20 #: netbox/templates/dcim/consoleserverport.html:20 -#: netbox/templates/dcim/device.html:15 netbox/templates/dcim/device.html:130 +#: netbox/templates/dcim/device.html:15 netbox/templates/dcim/device.html:136 #: netbox/templates/dcim/device_edit.html:12 #: netbox/templates/dcim/devicebay.html:20 #: netbox/templates/dcim/devicebay.html:48 @@ -3047,7 +3048,7 @@ msgstr "" msgid "Reserved" msgstr "" -#: netbox/dcim/choices.py:101 netbox/templates/dcim/device.html:259 +#: netbox/dcim/choices.py:101 netbox/templates/dcim/device.html:265 msgid "Available" msgstr "" @@ -3120,14 +3121,14 @@ msgstr "" msgid "Child" msgstr "" -#: netbox/dcim/choices.py:167 netbox/templates/dcim/device.html:349 +#: netbox/dcim/choices.py:167 netbox/templates/dcim/device.html:355 #: netbox/templates/dcim/rack.html:133 #: netbox/templates/dcim/rack_elevation_list.html:20 #: netbox/templates/dcim/rackreservation.html:76 msgid "Front" msgstr "" -#: netbox/dcim/choices.py:168 netbox/templates/dcim/device.html:355 +#: netbox/dcim/choices.py:168 netbox/templates/dcim/device.html:361 #: netbox/templates/dcim/rack.html:139 #: netbox/templates/dcim/rack_elevation_list.html:21 #: netbox/templates/dcim/rackreservation.html:82 @@ -3729,7 +3730,7 @@ msgstr "" #: netbox/dcim/filtersets.py:1591 netbox/dcim/forms/filtersets.py:111 #: netbox/dcim/tables/devices.py:216 netbox/netbox/navigation/menu.py:79 -#: netbox/templates/dcim/device.html:120 +#: netbox/templates/dcim/device.html:31 netbox/templates/dcim/device.html:126 #: netbox/templates/dcim/device_edit.html:95 #: netbox/templates/dcim/virtualchassis.html:20 #: netbox/templates/dcim/virtualchassis_add.html:12 @@ -3981,7 +3982,7 @@ msgstr "" #: netbox/dcim/forms/object_create.py:208 #: netbox/dcim/forms/object_create.py:357 netbox/dcim/tables/devices.py:175 #: netbox/dcim/tables/devices.py:747 netbox/dcim/tables/devicetypes.py:253 -#: netbox/templates/dcim/device.html:43 netbox/templates/dcim/device.html:131 +#: netbox/templates/dcim/device.html:49 netbox/templates/dcim/device.html:137 #: netbox/templates/dcim/modulebay.html:38 #: netbox/templates/dcim/virtualchassis.html:66 #: netbox/templates/dcim/virtualchassis_edit.html:59 @@ -4104,7 +4105,7 @@ msgstr "" #: netbox/extras/forms/bulk_import.py:238 netbox/extras/forms/filtersets.py:66 #: netbox/extras/forms/filtersets.py:160 netbox/extras/forms/filtersets.py:254 #: netbox/extras/forms/filtersets.py:284 netbox/extras/forms/model_forms.py:572 -#: netbox/ipam/forms/bulk_edit.py:193 netbox/templates/dcim/device.html:324 +#: netbox/ipam/forms/bulk_edit.py:193 netbox/templates/dcim/device.html:330 #: netbox/templates/dcim/devicetype.html:49 #: netbox/templates/dcim/moduletype.html:51 netbox/templates/dcim/rack.html:81 #: netbox/templates/dcim/racktype.html:41 @@ -4142,7 +4143,7 @@ msgid "Outer Dimensions" msgstr "" #: netbox/dcim/forms/bulk_edit.py:316 netbox/dcim/forms/model_forms.py:234 -#: netbox/dcim/forms/model_forms.py:315 netbox/templates/dcim/device.html:315 +#: netbox/dcim/forms/model_forms.py:315 netbox/templates/dcim/device.html:321 #: netbox/templates/dcim/inc/panels/racktype_dimensions.html:3 msgid "Dimensions" msgstr "" @@ -4159,7 +4160,7 @@ msgid "Rack type" msgstr "" #: netbox/dcim/forms/bulk_edit.py:384 netbox/dcim/forms/bulk_edit.py:765 -#: netbox/dcim/forms/bulk_edit.py:826 netbox/templates/dcim/device.html:104 +#: netbox/dcim/forms/bulk_edit.py:826 netbox/templates/dcim/device.html:110 #: netbox/templates/dcim/module.html:77 netbox/templates/dcim/modulebay.html:70 #: netbox/templates/dcim/rack.html:57 #: netbox/templates/virtualization/virtualmachine.html:35 @@ -4177,7 +4178,7 @@ msgstr "" #: netbox/dcim/forms/bulk_import.py:295 netbox/dcim/forms/bulk_import.py:453 #: netbox/dcim/forms/bulk_import.py:638 netbox/dcim/forms/filtersets.py:282 #: netbox/dcim/forms/filtersets.py:513 netbox/dcim/forms/filtersets.py:684 -#: netbox/dcim/forms/filtersets.py:824 netbox/templates/dcim/device.html:98 +#: netbox/dcim/forms/filtersets.py:824 netbox/templates/dcim/device.html:104 #: netbox/templates/dcim/devicetype.html:65 #: netbox/templates/dcim/moduletype.html:47 netbox/templates/dcim/rack.html:65 #: netbox/templates/dcim/racktype.html:28 @@ -4196,7 +4197,7 @@ msgstr "" #: netbox/dcim/forms/model_forms.py:583 netbox/dcim/forms/model_forms.py:861 #: netbox/dcim/forms/object_create.py:404 netbox/dcim/tables/devices.py:171 #: netbox/dcim/tables/power.py:70 netbox/dcim/tables/racks.py:225 -#: netbox/ipam/forms/filtersets.py:467 netbox/templates/dcim/device.html:30 +#: netbox/ipam/forms/filtersets.py:467 netbox/templates/dcim/device.html:36 #: netbox/templates/dcim/inc/cable_termination.html:16 #: netbox/templates/dcim/powerfeed.html:28 netbox/templates/dcim/rack.html:13 #: netbox/templates/dcim/rack/base.html:4 @@ -4239,7 +4240,7 @@ msgstr "" #: netbox/dcim/forms/model_forms.py:1083 netbox/dcim/forms/model_forms.py:1111 #: netbox/dcim/forms/model_forms.py:1142 netbox/dcim/forms/model_forms.py:1161 #: netbox/dcim/forms/model_forms.py:1179 netbox/dcim/forms/object_create.py:123 -#: netbox/dcim/tables/devicetypes.py:82 netbox/templates/dcim/device.html:88 +#: netbox/dcim/tables/devicetypes.py:82 netbox/templates/dcim/device.html:94 #: netbox/templates/dcim/devicebay.html:52 netbox/templates/dcim/module.html:61 msgid "Device Type" msgstr "" @@ -4310,7 +4311,7 @@ msgstr "" #: netbox/dcim/forms/bulk_edit.py:748 netbox/dcim/forms/bulk_import.py:556 #: netbox/dcim/forms/filtersets.py:816 netbox/dcim/forms/model_forms.py:555 #: netbox/dcim/forms/model_forms.py:618 netbox/dcim/tables/devices.py:192 -#: netbox/extras/filtersets.py:656 netbox/templates/dcim/device.html:186 +#: netbox/extras/filtersets.py:656 netbox/templates/dcim/device.html:192 #: netbox/templates/dcim/platform.html:26 #: netbox/templates/virtualization/virtualmachine.html:27 #: netbox/virtualization/forms/bulk_edit.py:142 @@ -4326,7 +4327,7 @@ msgstr "" #: netbox/dcim/forms/model_forms.py:627 netbox/dcim/tables/devices.py:212 #: netbox/extras/filtersets.py:689 netbox/extras/forms/filtersets.py:364 #: netbox/ipam/forms/filtersets.py:439 netbox/ipam/forms/filtersets.py:472 -#: netbox/templates/dcim/device.html:239 +#: netbox/templates/dcim/device.html:245 #: netbox/templates/virtualization/cluster.html:10 #: netbox/templates/virtualization/virtualmachine.html:92 #: netbox/templates/virtualization/virtualmachine.html:101 @@ -5085,7 +5086,7 @@ msgid "{side_upper} side termination not found: {device} {name}" msgstr "" #: netbox/dcim/forms/bulk_import.py:1461 netbox/dcim/forms/model_forms.py:891 -#: netbox/dcim/tables/devices.py:1065 netbox/templates/dcim/device.html:132 +#: netbox/dcim/tables/devices.py:1065 netbox/templates/dcim/device.html:138 #: netbox/templates/dcim/virtualchassis.html:27 #: netbox/templates/dcim/virtualchassis.html:67 msgid "Master" @@ -5116,7 +5117,7 @@ msgid "Single or three-phase" msgstr "" #: netbox/dcim/forms/bulk_import.py:1607 netbox/dcim/forms/model_forms.py:1847 -#: netbox/templates/dcim/device.html:190 +#: netbox/templates/dcim/device.html:196 #: netbox/templates/dcim/virtualdevicecontext.html:30 #: netbox/templates/virtualization/virtualmachine.html:52 msgid "Primary IPv4" @@ -5127,7 +5128,7 @@ msgid "IPv4 address with mask, e.g. 1.2.3.4/24" msgstr "" #: netbox/dcim/forms/bulk_import.py:1614 netbox/dcim/forms/model_forms.py:1856 -#: netbox/templates/dcim/device.html:206 +#: netbox/templates/dcim/device.html:212 #: netbox/templates/dcim/virtualdevicecontext.html:41 #: netbox/templates/virtualization/virtualmachine.html:68 msgid "Primary IPv6" @@ -7233,7 +7234,7 @@ msgid "VMs" msgstr "" #: netbox/dcim/tables/devices.py:111 netbox/dcim/tables/devices.py:226 -#: netbox/extras/forms/model_forms.py:712 netbox/templates/dcim/device.html:112 +#: netbox/extras/forms/model_forms.py:712 netbox/templates/dcim/device.html:118 #: netbox/templates/dcim/devicerole.html:48 #: netbox/templates/dcim/platform.html:41 #: netbox/templates/extras/configtemplate.html:10 @@ -7576,7 +7577,7 @@ msgid "Racks" msgstr "" #: netbox/dcim/tables/racks.py:63 netbox/dcim/tables/racks.py:145 -#: netbox/templates/dcim/device.html:318 +#: netbox/templates/dcim/device.html:324 #: netbox/templates/dcim/inc/panels/racktype_dimensions.html:14 msgid "Height" msgstr "" @@ -10887,7 +10888,7 @@ msgstr "" #: netbox/ipam/tables/ip.py:78 netbox/ipam/tables/ip.py:222 #: netbox/ipam/tables/ip.py:281 netbox/ipam/tables/vlans.py:55 -#: netbox/templates/dcim/device.html:260 +#: netbox/templates/dcim/device.html:266 #: netbox/templates/ipam/aggregate.html:24 #: netbox/templates/ipam/iprange.html:37 netbox/templates/ipam/prefix.html:102 msgid "Utilization" @@ -11170,7 +11171,7 @@ msgstr "" msgid "Tab" msgstr "" -#: netbox/netbox/choices.py:193 netbox/templates/dcim/device.html:327 +#: netbox/netbox/choices.py:193 netbox/templates/dcim/device.html:333 #: netbox/templates/dcim/rack.html:107 msgid "Kilograms" msgstr "" @@ -11179,7 +11180,7 @@ msgstr "" msgid "Grams" msgstr "" -#: netbox/netbox/choices.py:195 netbox/templates/dcim/device.html:328 +#: netbox/netbox/choices.py:195 netbox/templates/dcim/device.html:334 #: netbox/templates/dcim/rack.html:108 msgid "Pounds" msgstr "" @@ -11527,7 +11528,7 @@ msgstr "" msgid "Modules" msgstr "" -#: netbox/netbox/navigation/menu.py:80 netbox/templates/dcim/device.html:160 +#: netbox/netbox/navigation/menu.py:80 netbox/templates/dcim/device.html:166 #: netbox/templates/dcim/virtualdevicecontext.html:8 msgid "Virtual Device Contexts" msgstr "" @@ -11605,7 +11606,7 @@ msgstr "" msgid "Service Templates" msgstr "" -#: netbox/netbox/navigation/menu.py:213 netbox/templates/dcim/device.html:302 +#: netbox/netbox/navigation/menu.py:213 netbox/templates/dcim/device.html:308 #: netbox/templates/ipam/ipaddress.html:118 #: netbox/templates/virtualization/virtualmachine.html:154 msgid "Services" @@ -13079,86 +13080,86 @@ msgstr "" msgid "Not Connected" msgstr "" -#: netbox/templates/dcim/device.html:34 +#: netbox/templates/dcim/device.html:40 msgid "Highlight device in rack" msgstr "" -#: netbox/templates/dcim/device.html:55 +#: netbox/templates/dcim/device.html:61 msgid "Not racked" msgstr "" -#: netbox/templates/dcim/device.html:62 netbox/templates/dcim/site.html:94 +#: netbox/templates/dcim/device.html:68 netbox/templates/dcim/site.html:94 msgid "GPS Coordinates" msgstr "" -#: netbox/templates/dcim/device.html:68 netbox/templates/dcim/site.html:81 +#: netbox/templates/dcim/device.html:74 netbox/templates/dcim/site.html:81 #: netbox/templates/dcim/site.html:100 msgid "Map" msgstr "" -#: netbox/templates/dcim/device.html:108 +#: netbox/templates/dcim/device.html:114 #: netbox/templates/dcim/inventoryitem.html:60 #: netbox/templates/dcim/module.html:81 netbox/templates/dcim/modulebay.html:74 #: netbox/templates/dcim/rack.html:61 msgid "Asset Tag" msgstr "" -#: netbox/templates/dcim/device.html:123 +#: netbox/templates/dcim/device.html:129 msgid "View Virtual Chassis" msgstr "" -#: netbox/templates/dcim/device.html:164 +#: netbox/templates/dcim/device.html:170 msgid "Create VDC" msgstr "" -#: netbox/templates/dcim/device.html:175 +#: netbox/templates/dcim/device.html:181 #: netbox/templates/dcim/device_edit.html:66 #: netbox/virtualization/forms/model_forms.py:230 msgid "Management" msgstr "" -#: netbox/templates/dcim/device.html:195 netbox/templates/dcim/device.html:211 -#: netbox/templates/dcim/device.html:227 +#: netbox/templates/dcim/device.html:201 netbox/templates/dcim/device.html:217 +#: netbox/templates/dcim/device.html:233 #: netbox/templates/virtualization/virtualmachine.html:57 #: netbox/templates/virtualization/virtualmachine.html:73 msgid "NAT for" msgstr "" -#: netbox/templates/dcim/device.html:197 netbox/templates/dcim/device.html:213 -#: netbox/templates/dcim/device.html:229 +#: netbox/templates/dcim/device.html:203 netbox/templates/dcim/device.html:219 +#: netbox/templates/dcim/device.html:235 #: netbox/templates/virtualization/virtualmachine.html:59 #: netbox/templates/virtualization/virtualmachine.html:75 msgid "NAT" msgstr "" -#: netbox/templates/dcim/device.html:252 netbox/templates/dcim/rack.html:73 +#: netbox/templates/dcim/device.html:258 netbox/templates/dcim/rack.html:73 msgid "Power Utilization" msgstr "" -#: netbox/templates/dcim/device.html:256 +#: netbox/templates/dcim/device.html:262 msgid "Input" msgstr "" -#: netbox/templates/dcim/device.html:257 +#: netbox/templates/dcim/device.html:263 msgid "Outlets" msgstr "" -#: netbox/templates/dcim/device.html:258 +#: netbox/templates/dcim/device.html:264 msgid "Allocated" msgstr "" -#: netbox/templates/dcim/device.html:268 netbox/templates/dcim/device.html:270 -#: netbox/templates/dcim/device.html:286 +#: netbox/templates/dcim/device.html:274 netbox/templates/dcim/device.html:276 +#: netbox/templates/dcim/device.html:292 #: netbox/templates/dcim/powerfeed.html:67 msgid "VA" msgstr "" -#: netbox/templates/dcim/device.html:280 +#: netbox/templates/dcim/device.html:286 msgctxt "Leg of a power feed" msgid "Leg" msgstr "" -#: netbox/templates/dcim/device.html:306 +#: netbox/templates/dcim/device.html:312 #: netbox/templates/virtualization/virtualmachine.html:158 msgid "Add a service" msgstr "" From 4795fab16f11b59ff80dafd91a58323b47f75bed Mon Sep 17 00:00:00 2001 From: Aaron <56231533+aq5747@users.noreply.github.com> Date: Thu, 15 May 2025 10:40:03 -0400 Subject: [PATCH 07/30] Fixes #19486: Fix connection card rendering for Console Server Ports (#19498) This fixes a visual anomaly with the console server port details page, where cards are inadvertantly nested inside each other. --- netbox/templates/dcim/consoleserverport.html | 1 - 1 file changed, 1 deletion(-) diff --git a/netbox/templates/dcim/consoleserverport.html b/netbox/templates/dcim/consoleserverport.html index 72dd12a88..41d74e0eb 100644 --- a/netbox/templates/dcim/consoleserverport.html +++ b/netbox/templates/dcim/consoleserverport.html @@ -53,7 +53,6 @@

{% trans "Connection" %}

-
{% if object.mark_connected %}
From b6c8502408c5f393a50db2426ceb31d276b8e696 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 16 May 2025 05:02:28 +0000 Subject: [PATCH 08/30] Update source translation strings --- netbox/translations/en/LC_MESSAGES/django.po | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index 0dee0d32f..c5348913d 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-05-15 05:02+0000\n" +"POT-Creation-Date: 2025-05-16 05:02+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -5475,7 +5475,7 @@ msgstr "" #: netbox/dcim/forms/model_forms.py:1290 netbox/dcim/forms/model_forms.py:1761 #: netbox/dcim/tables/connections.py:27 #: netbox/templates/dcim/consoleport.html:17 -#: netbox/templates/dcim/consoleserverport.html:74 +#: netbox/templates/dcim/consoleserverport.html:73 #: netbox/templates/dcim/frontport.html:112 msgid "Console Port" msgstr "" @@ -5490,7 +5490,7 @@ msgstr "" #: netbox/dcim/forms/model_forms.py:1292 netbox/dcim/forms/model_forms.py:1763 #: netbox/templates/circuits/inc/circuit_termination_fields.html:53 #: netbox/templates/dcim/consoleport.html:76 -#: netbox/templates/dcim/consoleserverport.html:77 +#: netbox/templates/dcim/consoleserverport.html:76 #: netbox/templates/dcim/frontport.html:17 #: netbox/templates/dcim/frontport.html:115 #: netbox/templates/dcim/interface.html:244 @@ -5502,7 +5502,7 @@ msgstr "" #: netbox/dcim/tables/devices.py:750 #: netbox/templates/circuits/inc/circuit_termination_fields.html:54 #: netbox/templates/dcim/consoleport.html:79 -#: netbox/templates/dcim/consoleserverport.html:80 +#: netbox/templates/dcim/consoleserverport.html:79 #: netbox/templates/dcim/frontport.html:50 #: netbox/templates/dcim/frontport.html:118 #: netbox/templates/dcim/interface.html:247 @@ -12509,7 +12509,7 @@ msgstr "" #: netbox/templates/circuits/inc/circuit_termination_fields.html:20 #: netbox/templates/dcim/consoleport.html:59 -#: netbox/templates/dcim/consoleserverport.html:60 +#: netbox/templates/dcim/consoleserverport.html:59 #: netbox/templates/dcim/powerfeed.html:114 msgid "Marked as connected" msgstr "" @@ -12553,7 +12553,7 @@ msgstr "" #: netbox/templates/circuits/inc/circuit_termination_fields.html:49 #: netbox/templates/dcim/consoleport.html:69 -#: netbox/templates/dcim/consoleserverport.html:70 +#: netbox/templates/dcim/consoleserverport.html:69 #: netbox/templates/dcim/frontport.html:102 #: netbox/templates/dcim/interface.html:237 #: netbox/templates/dcim/interface.html:257 @@ -13072,7 +13072,7 @@ msgid "Rename Selected" msgstr "" #: netbox/templates/dcim/consoleport.html:65 -#: netbox/templates/dcim/consoleserverport.html:66 +#: netbox/templates/dcim/consoleserverport.html:65 #: netbox/templates/dcim/frontport.html:98 #: netbox/templates/dcim/interface.html:233 #: netbox/templates/dcim/poweroutlet.html:83 From 83dc65acb51d9ba0d8e9682c75716f227e78cb25 Mon Sep 17 00:00:00 2001 From: larsen0815 Date: Fri, 16 May 2025 12:19:07 +0200 Subject: [PATCH 09/30] Improve upgrade instructions --- docs/installation/upgrading.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md index f9a7a3189..84b133c4f 100644 --- a/docs/installation/upgrading.md +++ b/docs/installation/upgrading.md @@ -122,7 +122,7 @@ sudo cp /opt/netbox-$OLDVER/gunicorn.py /opt/netbox/ ### Option B: Check Out a Git Release -This guide assumes that NetBox is installed at `/opt/netbox`. First, determine the latest release either by visiting our [releases page](https://github.com/netbox-community/netbox/releases) or by running the following command: +This guide assumes that NetBox is installed in `/opt/netbox`. First, determine the latest release either by visiting our [releases page](https://github.com/netbox-community/netbox/releases) or by running the following command: ``` git ls-remote --tags https://github.com/netbox-community/netbox.git \ @@ -134,7 +134,9 @@ git ls-remote --tags https://github.com/netbox-community/netbox.git \ Check out the desired release by specifying its tag. For example: ``` -sudo git checkout v4.2.7 +cd /opt/netbox && \ +sudo git fetch && \ +sudo git checkout v2.4.7 ``` ## 4. Run the Upgrade Script From 21f5fe873c69ca972ce78dee3478466560cb40ea Mon Sep 17 00:00:00 2001 From: larsen0815 Date: Fri, 16 May 2025 15:09:52 +0200 Subject: [PATCH 10/30] Fixes typo --- docs/installation/upgrading.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md index 84b133c4f..21ffa9766 100644 --- a/docs/installation/upgrading.md +++ b/docs/installation/upgrading.md @@ -136,7 +136,7 @@ Check out the desired release by specifying its tag. For example: ``` cd /opt/netbox && \ sudo git fetch && \ -sudo git checkout v2.4.7 +sudo git checkout v4.2.7 ``` ## 4. Run the Upgrade Script From e6d364b250bbdd66adb0f567b39721e346f1ece0 Mon Sep 17 00:00:00 2001 From: Omri Abu <6192223+Omripresent@users.noreply.github.com> Date: Fri, 16 May 2025 14:46:43 -0400 Subject: [PATCH 11/30] Initilize error_message to empty string Update template branching for empty template render output --- netbox/extras/views.py | 2 +- netbox/templates/extras/object_render_config.html | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/netbox/extras/views.py b/netbox/extras/views.py index ae9337779..ca4596fe8 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -966,7 +966,7 @@ class ObjectRenderConfigView(generic.ObjectView): # Render the config template rendered_config = None - error_message = None + error_message = '' if config_template := instance.get_config_template(): try: rendered_config = config_template.render(context=context_data) diff --git a/netbox/templates/extras/object_render_config.html b/netbox/templates/extras/object_render_config.html index 42c6c1b86..10d6d4aef 100644 --- a/netbox/templates/extras/object_render_config.html +++ b/netbox/templates/extras/object_render_config.html @@ -63,11 +63,15 @@
{{ rendered_config }}
- {% else %} + {% elif error_message %}

{% trans "Error rendering template" %}

{% trans error_message %}
+ {% else %} +
+

{% trans "Template output is empty" %}

+
{% endif %} {% else %}
From 03ff535772048d90e6d0b53d562339fdc52a43e4 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Fri, 16 May 2025 12:58:55 -0500 Subject: [PATCH 12/30] Fixes #19510: Re-adds IPAddressType.assigned filter --- netbox/ipam/graphql/filters.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/netbox/ipam/graphql/filters.py b/netbox/ipam/graphql/filters.py index 511d4d53c..7421a935a 100644 --- a/netbox/ipam/graphql/filters.py +++ b/netbox/ipam/graphql/filters.py @@ -142,6 +142,10 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter nat_outside_id: ID | None = strawberry_django.filter_field() dns_name: FilterLookup[str] | None = strawberry_django.filter_field() + @strawberry_django.filter_field() + def assigned(self, value: bool, prefix) -> Q: + return Q(assigned_object_id__isnull=(not value)) + @strawberry_django.filter_field() def parent(self, value: list[str], prefix) -> Q: if not value: From a2a8779ebc1ba1141f955b71d5afcc6bbb2dfc07 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Mon, 19 May 2025 07:38:30 -0500 Subject: [PATCH 13/30] Fixes #19415: Increased Circuit/WirelessLink distance upper limit (#19495) * Fixes #19415: Increased Circuit/WirelessLink absolute distance upper limit Also adds form validation that provides a useful message to the user rather than a 500 error with potentially little information. * Include forgotten migration files * Remove unnecessary comments * Remove more unnecessary comments * Addresses PR feedback * Gah, remove django migration header comment * Clean up new has_field_errors mechanism, fix issue with ObjectAttribute * Address PR feedback, revert changes to render_fieldset template tag --- netbox/circuits/forms/model_forms.py | 3 ++- ...52_extend_circuit_abs_distance_upper_limit.py | 16 ++++++++++++++++ netbox/netbox/models/mixins.py | 5 +++-- netbox/utilities/forms/mixins.py | 13 +++++++++++++ .../templates/form_helpers/render_fieldset.html | 5 +++++ netbox/wireless/forms/model_forms.py | 3 ++- ...end_wireless_link_abs_distance_upper_limit.py | 16 ++++++++++++++++ 7 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 netbox/circuits/migrations/0052_extend_circuit_abs_distance_upper_limit.py create mode 100644 netbox/wireless/migrations/0015_extend_wireless_link_abs_distance_upper_limit.py diff --git a/netbox/circuits/forms/model_forms.py b/netbox/circuits/forms/model_forms.py index 6f8ab783d..ce09862ae 100644 --- a/netbox/circuits/forms/model_forms.py +++ b/netbox/circuits/forms/model_forms.py @@ -16,6 +16,7 @@ from utilities.forms import get_field_value from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, ) +from utilities.forms.mixins import DistanceValidationMixin from utilities.forms.rendering import FieldSet, InlineFields from utilities.forms.widgets import DatePicker, HTMXSelect, NumberWithOptions from utilities.templatetags.builtins.filters import bettertitle @@ -105,7 +106,7 @@ class CircuitTypeForm(NetBoxModelForm): ] -class CircuitForm(TenancyForm, NetBoxModelForm): +class CircuitForm(DistanceValidationMixin, TenancyForm, NetBoxModelForm): provider = DynamicModelChoiceField( label=_('Provider'), queryset=Provider.objects.all(), diff --git a/netbox/circuits/migrations/0052_extend_circuit_abs_distance_upper_limit.py b/netbox/circuits/migrations/0052_extend_circuit_abs_distance_upper_limit.py new file mode 100644 index 000000000..abc54f627 --- /dev/null +++ b/netbox/circuits/migrations/0052_extend_circuit_abs_distance_upper_limit.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('circuits', '0051_virtualcircuit_group_assignment'), + ] + + operations = [ + migrations.AlterField( + model_name='circuit', + name='_abs_distance', + field=models.DecimalField(blank=True, decimal_places=4, max_digits=13, null=True), + ), + ] diff --git a/netbox/netbox/models/mixins.py b/netbox/netbox/models/mixins.py index dc706c7c2..13af8aaf5 100644 --- a/netbox/netbox/models/mixins.py +++ b/netbox/netbox/models/mixins.py @@ -1,6 +1,7 @@ from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import gettext_lazy as _ + from netbox.choices import * from utilities.conversion import to_grams, to_meters @@ -58,7 +59,7 @@ class DistanceMixin(models.Model): max_digits=8, decimal_places=2, blank=True, - null=True + null=True, ) distance_unit = models.CharField( verbose_name=_('distance unit'), @@ -69,7 +70,7 @@ class DistanceMixin(models.Model): ) # Stores the normalized distance (in meters) for database ordering _abs_distance = models.DecimalField( - max_digits=10, + max_digits=13, decimal_places=4, blank=True, null=True diff --git a/netbox/utilities/forms/mixins.py b/netbox/utilities/forms/mixins.py index e89fbb520..ca0f64e54 100644 --- a/netbox/utilities/forms/mixins.py +++ b/netbox/utilities/forms/mixins.py @@ -1,10 +1,13 @@ import time +from decimal import Decimal from django import forms +from django.core.validators import MaxValueValidator, MinValueValidator from django.utils.translation import gettext_lazy as _ __all__ = ( 'CheckLastUpdatedMixin', + 'DistanceValidationMixin', ) @@ -44,3 +47,13 @@ class CheckLastUpdatedMixin(forms.Form): "This object has been modified since the form was rendered. Please consult the object's change " "log for details." )) + + +class DistanceValidationMixin(forms.Form): + distance = forms.DecimalField( + required=False, + validators=[ + MinValueValidator(Decimal(0)), + MaxValueValidator(Decimal(100000)), + ] + ) diff --git a/netbox/utilities/templates/form_helpers/render_fieldset.html b/netbox/utilities/templates/form_helpers/render_fieldset.html index ae8252b97..1821a3cb7 100644 --- a/netbox/utilities/templates/form_helpers/render_fieldset.html +++ b/netbox/utilities/templates/form_helpers/render_fieldset.html @@ -31,6 +31,11 @@
{{ field }}
{% trans field.label %}
+ {% if field.errors %} +
+ {% for error in field.errors %}{{ error }}{% if not forloop.last %}
{% endif %}{% endfor %} +
+ {% endif %}
{% endfor %}
diff --git a/netbox/wireless/forms/model_forms.py b/netbox/wireless/forms/model_forms.py index 56422ab57..08f418e3c 100644 --- a/netbox/wireless/forms/model_forms.py +++ b/netbox/wireless/forms/model_forms.py @@ -7,6 +7,7 @@ from ipam.models import VLAN from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.forms.fields import CommentField, DynamicModelChoiceField, SlugField +from utilities.forms.mixins import DistanceValidationMixin from utilities.forms.rendering import FieldSet, InlineFields from wireless.models import * @@ -73,7 +74,7 @@ class WirelessLANForm(ScopedForm, TenancyForm, NetBoxModelForm): } -class WirelessLinkForm(TenancyForm, NetBoxModelForm): +class WirelessLinkForm(DistanceValidationMixin, TenancyForm, NetBoxModelForm): site_a = DynamicModelChoiceField( queryset=Site.objects.all(), required=False, diff --git a/netbox/wireless/migrations/0015_extend_wireless_link_abs_distance_upper_limit.py b/netbox/wireless/migrations/0015_extend_wireless_link_abs_distance_upper_limit.py new file mode 100644 index 000000000..86e8c6af5 --- /dev/null +++ b/netbox/wireless/migrations/0015_extend_wireless_link_abs_distance_upper_limit.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wireless', '0014_wirelesslangroup_comments'), + ] + + operations = [ + migrations.AlterField( + model_name='wirelesslink', + name='_abs_distance', + field=models.DecimalField(blank=True, decimal_places=4, max_digits=13, null=True), + ), + ] From 88565e8f680a5b64f1a22b67f007f15acf8f1373 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 05:02:15 +0000 Subject: [PATCH 14/30] Update source translation strings --- netbox/translations/en/LC_MESSAGES/django.po | 114 +++++++++---------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index c5348913d..349d4a68a 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-05-16 05:02+0000\n" +"POT-Creation-Date: 2025-05-20 05:01+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -252,8 +252,8 @@ msgstr "" #: netbox/virtualization/forms/model_forms.py:178 #: netbox/virtualization/tables/virtualmachines.py:33 #: netbox/vpn/forms/filtersets.py:277 netbox/wireless/forms/filtersets.py:88 -#: netbox/wireless/forms/model_forms.py:80 -#: netbox/wireless/forms/model_forms.py:122 +#: netbox/wireless/forms/model_forms.py:81 +#: netbox/wireless/forms/model_forms.py:123 msgid "Site" msgstr "" @@ -369,9 +369,9 @@ msgstr "" #: netbox/circuits/forms/filtersets.py:224 #: netbox/circuits/forms/filtersets.py:251 #: netbox/circuits/forms/filtersets.py:297 -#: netbox/circuits/forms/model_forms.py:139 -#: netbox/circuits/forms/model_forms.py:162 -#: netbox/circuits/forms/model_forms.py:262 +#: netbox/circuits/forms/model_forms.py:140 +#: netbox/circuits/forms/model_forms.py:163 +#: netbox/circuits/forms/model_forms.py:263 #: netbox/circuits/tables/circuits.py:107 #: netbox/circuits/tables/circuits.py:202 netbox/dcim/forms/connections.py:73 #: netbox/templates/circuits/circuit.html:15 @@ -434,8 +434,8 @@ msgstr "" #: netbox/circuits/forms/bulk_import.py:249 #: netbox/circuits/forms/filtersets.py:373 #: netbox/circuits/forms/filtersets.py:379 -#: netbox/circuits/forms/model_forms.py:343 -#: netbox/circuits/forms/model_forms.py:358 +#: netbox/circuits/forms/model_forms.py:344 +#: netbox/circuits/forms/model_forms.py:359 #: netbox/circuits/tables/virtual_circuits.py:87 #: netbox/templates/circuits/virtualcircuit.html:20 #: netbox/templates/circuits/virtualcircuittermination.html:38 @@ -449,7 +449,7 @@ msgid "Interface (ID)" msgstr "" #: netbox/circuits/forms/bulk_edit.py:42 netbox/circuits/forms/filtersets.py:64 -#: netbox/circuits/forms/model_forms.py:42 +#: netbox/circuits/forms/model_forms.py:43 #: netbox/circuits/tables/providers.py:32 netbox/dcim/forms/bulk_edit.py:137 #: netbox/dcim/forms/filtersets.py:197 netbox/dcim/forms/model_forms.py:132 #: netbox/dcim/tables/sites.py:100 netbox/ipam/models/asns.py:123 @@ -620,9 +620,9 @@ msgstr "" #: netbox/circuits/forms/filtersets.py:338 #: netbox/circuits/forms/filtersets.py:374 #: netbox/circuits/forms/filtersets.py:397 -#: netbox/circuits/forms/model_forms.py:60 -#: netbox/circuits/forms/model_forms.py:76 -#: netbox/circuits/forms/model_forms.py:110 +#: netbox/circuits/forms/model_forms.py:61 +#: netbox/circuits/forms/model_forms.py:77 +#: netbox/circuits/forms/model_forms.py:111 #: netbox/circuits/tables/circuits.py:57 netbox/circuits/tables/circuits.py:111 #: netbox/circuits/tables/circuits.py:195 #: netbox/circuits/tables/providers.py:70 @@ -740,8 +740,8 @@ msgstr "" #: netbox/circuits/forms/bulk_import.py:214 #: netbox/circuits/forms/filtersets.py:151 #: netbox/circuits/forms/filtersets.py:346 -#: netbox/circuits/forms/model_forms.py:116 -#: netbox/circuits/forms/model_forms.py:330 +#: netbox/circuits/forms/model_forms.py:117 +#: netbox/circuits/forms/model_forms.py:331 #: netbox/templates/circuits/virtualcircuit.html:31 #: netbox/templates/circuits/virtualcircuittermination.html:34 msgid "Provider account" @@ -921,12 +921,12 @@ msgstr "" #: netbox/circuits/forms/bulk_edit.py:176 #: netbox/circuits/forms/filtersets.py:209 -#: netbox/circuits/forms/model_forms.py:136 +#: netbox/circuits/forms/model_forms.py:137 #: netbox/templates/circuits/circuit.html:38 #: netbox/templates/wireless/wirelesslink.html:38 #: netbox/wireless/forms/bulk_edit.py:133 #: netbox/wireless/forms/filtersets.py:130 -#: netbox/wireless/forms/model_forms.py:169 +#: netbox/wireless/forms/model_forms.py:170 msgid "Distance" msgstr "" @@ -942,7 +942,7 @@ msgid "Distance unit" msgstr "" #: netbox/circuits/forms/bulk_edit.py:196 -#: netbox/circuits/forms/model_forms.py:141 +#: netbox/circuits/forms/model_forms.py:142 msgid "Service Parameters" msgstr "" @@ -984,9 +984,9 @@ msgstr "" #: netbox/circuits/forms/bulk_edit.py:198 #: netbox/circuits/forms/bulk_edit.py:356 -#: netbox/circuits/forms/model_forms.py:142 -#: netbox/circuits/forms/model_forms.py:240 -#: netbox/circuits/forms/model_forms.py:345 +#: netbox/circuits/forms/model_forms.py:143 +#: netbox/circuits/forms/model_forms.py:241 +#: netbox/circuits/forms/model_forms.py:346 #: netbox/dcim/forms/model_forms.py:148 netbox/dcim/forms/model_forms.py:191 #: netbox/dcim/forms/model_forms.py:281 netbox/dcim/forms/model_forms.py:339 #: netbox/dcim/forms/model_forms.py:874 netbox/dcim/forms/model_forms.py:1869 @@ -1004,13 +1004,13 @@ msgstr "" #: netbox/virtualization/forms/model_forms.py:229 #: netbox/vpn/forms/bulk_edit.py:78 netbox/vpn/forms/filtersets.py:48 #: netbox/vpn/forms/model_forms.py:63 netbox/vpn/forms/model_forms.py:148 -#: netbox/vpn/forms/model_forms.py:414 netbox/wireless/forms/model_forms.py:58 -#: netbox/wireless/forms/model_forms.py:174 +#: netbox/vpn/forms/model_forms.py:414 netbox/wireless/forms/model_forms.py:59 +#: netbox/wireless/forms/model_forms.py:175 msgid "Tenancy" msgstr "" #: netbox/circuits/forms/bulk_edit.py:215 -#: netbox/circuits/forms/model_forms.py:170 +#: netbox/circuits/forms/model_forms.py:171 #: netbox/dcim/forms/bulk_import.py:1348 netbox/dcim/forms/bulk_import.py:1366 msgid "Termination type" msgstr "" @@ -1018,7 +1018,7 @@ msgstr "" #: netbox/circuits/forms/bulk_edit.py:218 #: netbox/circuits/forms/bulk_import.py:133 #: netbox/circuits/forms/filtersets.py:226 -#: netbox/circuits/forms/model_forms.py:173 +#: netbox/circuits/forms/model_forms.py:174 #: netbox/templates/circuits/inc/circuit_termination.html:6 #: netbox/templates/dcim/cable.html:68 netbox/templates/dcim/cable.html:72 #: netbox/vpn/forms/bulk_import.py:100 netbox/vpn/forms/filtersets.py:82 @@ -1042,7 +1042,7 @@ msgid "Mark connected" msgstr "" #: netbox/circuits/forms/bulk_edit.py:243 -#: netbox/circuits/forms/model_forms.py:184 +#: netbox/circuits/forms/model_forms.py:185 #: netbox/templates/circuits/inc/circuit_termination_fields.html:55 #: netbox/templates/dcim/frontport.html:121 #: netbox/templates/dcim/interface.html:250 @@ -1051,7 +1051,7 @@ msgid "Circuit Termination" msgstr "" #: netbox/circuits/forms/bulk_edit.py:245 -#: netbox/circuits/forms/model_forms.py:186 +#: netbox/circuits/forms/model_forms.py:187 msgid "Termination Details" msgstr "" @@ -1074,7 +1074,7 @@ msgstr "" #: netbox/circuits/forms/filtersets.py:264 #: netbox/circuits/forms/filtersets.py:354 #: netbox/circuits/forms/filtersets.py:392 -#: netbox/circuits/forms/model_forms.py:325 +#: netbox/circuits/forms/model_forms.py:326 #: netbox/circuits/tables/virtual_circuits.py:51 #: netbox/circuits/tables/virtual_circuits.py:98 msgid "Provider network" @@ -1083,7 +1083,7 @@ msgstr "" #: netbox/circuits/forms/bulk_edit.py:365 #: netbox/circuits/forms/bulk_import.py:254 #: netbox/circuits/forms/filtersets.py:382 -#: netbox/circuits/forms/model_forms.py:365 netbox/dcim/forms/bulk_edit.py:372 +#: netbox/circuits/forms/model_forms.py:366 netbox/dcim/forms/bulk_edit.py:372 #: netbox/dcim/forms/bulk_edit.py:1324 netbox/dcim/forms/bulk_edit.py:1760 #: netbox/dcim/forms/bulk_import.py:259 netbox/dcim/forms/bulk_import.py:1137 #: netbox/dcim/forms/filtersets.py:369 netbox/dcim/forms/filtersets.py:797 @@ -1207,7 +1207,7 @@ msgid "Operational role" msgstr "" #: netbox/circuits/forms/bulk_import.py:259 -#: netbox/circuits/forms/model_forms.py:368 +#: netbox/circuits/forms/model_forms.py:369 #: netbox/circuits/tables/virtual_circuits.py:111 #: netbox/dcim/forms/bulk_import.py:1268 netbox/dcim/forms/model_forms.py:1289 #: netbox/dcim/forms/model_forms.py:1558 netbox/dcim/forms/model_forms.py:1725 @@ -1231,8 +1231,8 @@ msgstr "" #: netbox/templates/wireless/wirelesslink.html:55 #: netbox/virtualization/forms/model_forms.py:377 #: netbox/vpn/forms/bulk_import.py:302 netbox/vpn/forms/model_forms.py:439 -#: netbox/vpn/forms/model_forms.py:448 netbox/wireless/forms/model_forms.py:117 -#: netbox/wireless/forms/model_forms.py:159 +#: netbox/vpn/forms/model_forms.py:448 netbox/wireless/forms/model_forms.py:118 +#: netbox/wireless/forms/model_forms.py:160 msgid "Interface" msgstr "" @@ -1274,8 +1274,8 @@ msgstr "" #: netbox/virtualization/forms/filtersets.py:80 #: netbox/virtualization/forms/filtersets.py:106 #: netbox/wireless/forms/filtersets.py:93 -#: netbox/wireless/forms/model_forms.py:91 -#: netbox/wireless/forms/model_forms.py:133 +#: netbox/wireless/forms/model_forms.py:92 +#: netbox/wireless/forms/model_forms.py:134 msgid "Location" msgstr "" @@ -1373,7 +1373,7 @@ msgid "Assignment" msgstr "" #: netbox/circuits/forms/filtersets.py:302 -#: netbox/circuits/forms/model_forms.py:252 +#: netbox/circuits/forms/model_forms.py:253 #: netbox/circuits/tables/circuits.py:190 netbox/dcim/forms/bulk_edit.py:126 #: netbox/dcim/forms/bulk_import.py:103 netbox/dcim/forms/model_forms.py:125 #: netbox/dcim/tables/sites.py:95 netbox/extras/forms/filtersets.py:544 @@ -1407,21 +1407,21 @@ msgstr "" #: netbox/vpn/tables/tunnels.py:44 netbox/wireless/forms/bulk_edit.py:51 #: netbox/wireless/forms/bulk_import.py:38 #: netbox/wireless/forms/filtersets.py:49 -#: netbox/wireless/forms/model_forms.py:42 +#: netbox/wireless/forms/model_forms.py:43 #: netbox/wireless/tables/wirelesslan.py:48 msgid "Group" msgstr "" -#: netbox/circuits/forms/model_forms.py:239 +#: netbox/circuits/forms/model_forms.py:240 #: netbox/templates/circuits/circuitgroup.html:25 msgid "Circuit Group" msgstr "" -#: netbox/circuits/forms/model_forms.py:259 +#: netbox/circuits/forms/model_forms.py:260 msgid "Circuit type" msgstr "" -#: netbox/circuits/forms/model_forms.py:270 +#: netbox/circuits/forms/model_forms.py:271 msgid "Group Assignment" msgstr "" @@ -1841,7 +1841,7 @@ msgid "Circuit ID" msgstr "" #: netbox/circuits/tables/circuits.py:71 -#: netbox/wireless/forms/model_forms.py:164 +#: netbox/wireless/forms/model_forms.py:165 msgid "Side A" msgstr "" @@ -2000,8 +2000,8 @@ msgstr "" #: netbox/vpn/forms/bulk_import.py:86 netbox/vpn/forms/bulk_import.py:288 #: netbox/vpn/forms/filtersets.py:286 netbox/vpn/forms/model_forms.py:91 #: netbox/vpn/forms/model_forms.py:126 netbox/vpn/forms/model_forms.py:237 -#: netbox/vpn/forms/model_forms.py:456 netbox/wireless/forms/model_forms.py:103 -#: netbox/wireless/forms/model_forms.py:145 +#: netbox/vpn/forms/model_forms.py:456 netbox/wireless/forms/model_forms.py:104 +#: netbox/wireless/forms/model_forms.py:146 #: netbox/wireless/tables/wirelesslan.py:84 msgid "Device" msgstr "" @@ -3113,7 +3113,7 @@ msgstr "" #: netbox/virtualization/tables/virtualmachines.py:132 #: netbox/wireless/forms/bulk_edit.py:26 #: netbox/wireless/forms/bulk_import.py:23 -#: netbox/wireless/forms/model_forms.py:22 +#: netbox/wireless/forms/model_forms.py:23 msgid "Parent" msgstr "" @@ -3913,7 +3913,7 @@ msgid "Virtual Device Context (Identifier)" msgstr "" #: netbox/dcim/filtersets.py:1973 netbox/templates/wireless/wirelesslan.html:11 -#: netbox/wireless/forms/model_forms.py:56 +#: netbox/wireless/forms/model_forms.py:57 msgid "Wireless LAN" msgstr "" @@ -5361,7 +5361,7 @@ msgstr "" #: netbox/virtualization/forms/model_forms.py:79 #: netbox/virtualization/tables/clusters.py:80 #: netbox/wireless/forms/bulk_edit.py:94 netbox/wireless/forms/filtersets.py:37 -#: netbox/wireless/forms/model_forms.py:57 +#: netbox/wireless/forms/model_forms.py:58 #: netbox/wireless/tables/wirelesslan.py:58 msgid "Scope" msgstr "" @@ -7769,7 +7769,7 @@ msgstr "" #: netbox/extras/choices.py:108 netbox/templates/tenancy/contact.html:67 #: netbox/tenancy/forms/bulk_edit.py:125 -#: netbox/wireless/forms/model_forms.py:172 +#: netbox/wireless/forms/model_forms.py:173 msgid "Link" msgstr "" @@ -8650,7 +8650,7 @@ msgstr "" #: netbox/extras/models/configs.py:38 netbox/extras/models/models.py:315 #: netbox/extras/models/models.py:480 netbox/extras/models/models.py:559 #: netbox/extras/models/search.py:48 netbox/extras/models/tags.py:44 -#: netbox/ipam/models/ip.py:188 netbox/netbox/models/mixins.py:15 +#: netbox/ipam/models/ip.py:188 netbox/netbox/models/mixins.py:16 msgid "weight" msgstr "" @@ -9991,7 +9991,7 @@ msgstr "" #: netbox/vpn/forms/model_forms.py:436 netbox/vpn/forms/model_forms.py:455 #: netbox/wireless/forms/bulk_edit.py:58 #: netbox/wireless/forms/bulk_import.py:50 -#: netbox/wireless/forms/model_forms.py:51 netbox/wireless/models.py:102 +#: netbox/wireless/forms/model_forms.py:52 netbox/wireless/models.py:102 msgid "VLAN" msgstr "" @@ -10060,8 +10060,8 @@ msgstr "" #: netbox/wireless/forms/bulk_edit.py:95 netbox/wireless/forms/bulk_edit.py:153 #: netbox/wireless/forms/filtersets.py:39 #: netbox/wireless/forms/filtersets.py:104 -#: netbox/wireless/forms/model_forms.py:59 -#: netbox/wireless/forms/model_forms.py:175 +#: netbox/wireless/forms/model_forms.py:60 +#: netbox/wireless/forms/model_forms.py:176 msgid "Authentication" msgstr "" @@ -11471,23 +11471,23 @@ msgstr "" msgid "{class_name} must implement a sync_data() method." msgstr "" -#: netbox/netbox/models/mixins.py:22 +#: netbox/netbox/models/mixins.py:23 msgid "weight unit" msgstr "" -#: netbox/netbox/models/mixins.py:52 +#: netbox/netbox/models/mixins.py:53 msgid "Must specify a unit when setting a weight" msgstr "" -#: netbox/netbox/models/mixins.py:57 +#: netbox/netbox/models/mixins.py:58 msgid "distance" msgstr "" -#: netbox/netbox/models/mixins.py:64 +#: netbox/netbox/models/mixins.py:65 msgid "distance unit" msgstr "" -#: netbox/netbox/models/mixins.py:99 +#: netbox/netbox/models/mixins.py:100 msgid "Must specify a unit when setting a distance" msgstr "" @@ -15103,7 +15103,7 @@ msgid "Add Wireless LAN" msgstr "" #: netbox/templates/wireless/wirelesslangroup.html:26 -#: netbox/wireless/forms/model_forms.py:30 +#: netbox/wireless/forms/model_forms.py:31 msgid "Wireless LAN Group" msgstr "" @@ -15699,7 +15699,7 @@ msgstr "" msgid "Unrecognized header: {name}" msgstr "" -#: netbox/utilities/forms/mixins.py:44 +#: netbox/utilities/forms/mixins.py:47 msgid "" "This object has been modified since the form was rendered. Please consult " "the object's change log for details." @@ -16644,7 +16644,7 @@ msgstr "" msgid "Interface B" msgstr "" -#: netbox/wireless/forms/model_forms.py:165 +#: netbox/wireless/forms/model_forms.py:166 msgid "Side B" msgstr "" From 51d046b1f50b0cf8c56903ebefe43aa2caff13eb Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 21 May 2025 12:57:32 -0400 Subject: [PATCH 15/30] Closes #19521: Clean up test suite output (#19524) --- netbox/core/tests/test_api.py | 7 +++++-- netbox/core/tests/test_views.py | 8 +++++--- netbox/dcim/tests/test_api.py | 5 +++-- netbox/extras/tests/test_api.py | 9 ++++++++- netbox/extras/tests/test_scripts.py | 8 ++++++-- netbox/ipam/tests/test_api.py | 5 +++-- netbox/utilities/tests/test_api.py | 4 +++- netbox/virtualization/tests/test_api.py | 9 +++++++-- 8 files changed, 40 insertions(+), 15 deletions(-) diff --git a/netbox/core/tests/test_api.py b/netbox/core/tests/test_api.py index d8fb8fd83..e9e77f252 100644 --- a/netbox/core/tests/test_api.py +++ b/netbox/core/tests/test_api.py @@ -9,6 +9,7 @@ from rq.registry import FailedJobRegistry, StartedJobRegistry from users.models import Token, User from utilities.testing import APITestCase, APIViewTestCases, TestCase +from utilities.testing.utils import disable_logging from ..models import * @@ -189,7 +190,8 @@ class BackgroundTaskTestCase(TestCase): # Enqueue & run a job that will fail job = queue.enqueue(self.dummy_job_failing) worker = get_worker('default') - worker.work(burst=True) + with disable_logging(): + worker.work(burst=True) self.assertTrue(job.is_failed) # Re-enqueue the failed job and check that its status has been reset @@ -231,7 +233,8 @@ class BackgroundTaskTestCase(TestCase): self.assertEqual(job.get_status(), JobStatus.STARTED) response = self.client.post(reverse('core-api:rqtask-stop', args=[job.id]), **self.header) self.assertEqual(response.status_code, 200) - worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started + with disable_logging(): + worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started started_job_registry = StartedJobRegistry(queue.name, connection=queue.connection) self.assertEqual(len(started_job_registry), 0) diff --git a/netbox/core/tests/test_views.py b/netbox/core/tests/test_views.py index 047b51ef6..96a4292df 100644 --- a/netbox/core/tests/test_views.py +++ b/netbox/core/tests/test_views.py @@ -14,7 +14,7 @@ from core.choices import ObjectChangeActionChoices from core.models import * from dcim.models import Site from users.models import User -from utilities.testing import TestCase, ViewTestCases, create_tags +from utilities.testing import TestCase, ViewTestCases, create_tags, disable_logging class DataSourceTestCase(ViewTestCases.PrimaryObjectViewTestCase): @@ -271,7 +271,8 @@ class BackgroundTaskTestCase(TestCase): # Enqueue & run a job that will fail job = queue.enqueue(self.dummy_job_failing) worker = get_worker('default') - worker.work(burst=True) + with disable_logging(): + worker.work(burst=True) self.assertTrue(job.is_failed) # Re-enqueue the failed job and check that its status has been reset @@ -317,7 +318,8 @@ class BackgroundTaskTestCase(TestCase): self.assertEqual(len(started_job_registry), 1) response = self.client.get(reverse('core:background_task_stop', args=[job.id])) self.assertEqual(response.status_code, 302) - worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started + with disable_logging(): + worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started self.assertEqual(len(started_job_registry), 0) canceled_job_registry = FailedJobRegistry(queue.name, connection=queue.connection) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index c3ac6053d..8af539b04 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -14,7 +14,7 @@ from ipam.models import ASN, RIR, VLAN, VRF from netbox.api.serializers import GenericObjectSerializer from tenancy.models import Tenant from users.models import User -from utilities.testing import APITestCase, APIViewTestCases, create_test_device +from utilities.testing import APITestCase, APIViewTestCases, create_test_device, disable_logging from virtualization.models import Cluster, ClusterType from wireless.choices import WirelessChannelChoices from wireless.models import WirelessLAN @@ -1858,7 +1858,8 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase # Attempt to delete only the parent interface url = self._get_detail_url(interface1) - self.client.delete(url, **self.header) + with disable_logging(): + self.client.delete(url, **self.header) self.assertEqual(device.interfaces.count(), 4) # Parent was not deleted # Attempt to bulk delete parent & child together diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 6e3fb37fc..29af3f96d 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -2,7 +2,7 @@ import datetime from django.contrib.contenttypes.models import ContentType from django.urls import reverse -from django.utils.timezone import make_aware +from django.utils.timezone import make_aware, now from rest_framework import status from core.choices import ManagedFileRootPathChoices @@ -991,6 +991,10 @@ class SubscriptionTest(APIViewTestCases.APIViewTestCase): }, ] + cls.bulk_update_data = { + 'user': users[3].pk, + } + class NotificationGroupTest(APIViewTestCases.APIViewTestCase): model = NotificationGroup @@ -1072,6 +1076,9 @@ class NotificationGroupTest(APIViewTestCases.APIViewTestCase): class NotificationTest(APIViewTestCases.APIViewTestCase): model = Notification brief_fields = ['display', 'event_type', 'id', 'object_id', 'object_type', 'read', 'url', 'user'] + bulk_update_data = { + 'read': now(), + } @classmethod def setUpTestData(cls): diff --git a/netbox/extras/tests/test_scripts.py b/netbox/extras/tests/test_scripts.py index bed8f0fc5..17eb5a31a 100644 --- a/netbox/extras/tests/test_scripts.py +++ b/netbox/extras/tests/test_scripts.py @@ -1,3 +1,4 @@ +import logging import tempfile from datetime import date, datetime, timezone @@ -7,6 +8,7 @@ from netaddr import IPAddress, IPNetwork from dcim.models import DeviceRole from extras.scripts import * +from utilities.testing import disable_logging CHOICES = ( ('ff0000', 'Red'), @@ -39,7 +41,8 @@ class ScriptTest(TestCase): datafile.write(bytes(YAML_DATA, 'UTF-8')) datafile.seek(0) - data = Script().load_yaml(datafile.name) + with disable_logging(level=logging.WARNING): + data = Script().load_yaml(datafile.name) self.assertEqual(data, { 'Foo': 123, 'Bar': 456, @@ -51,7 +54,8 @@ class ScriptTest(TestCase): datafile.write(bytes(JSON_DATA, 'UTF-8')) datafile.seek(0) - data = Script().load_json(datafile.name) + with disable_logging(level=logging.WARNING): + data = Script().load_json(datafile.name) self.assertEqual(data, { 'Foo': 123, 'Bar': 456, diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index 4b9b340c4..6255aaf86 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -1,4 +1,5 @@ import json +import logging from django.urls import reverse from netaddr import IPNetwork @@ -9,7 +10,7 @@ from ipam.choices import * from ipam.models import * from tenancy.models import Tenant from utilities.data import string_to_ranges -from utilities.testing import APITestCase, APIViewTestCases, create_test_device, disable_warnings +from utilities.testing import APITestCase, APIViewTestCases, create_test_device, disable_logging class AppTest(APITestCase): @@ -1026,7 +1027,7 @@ class VLANTest(APIViewTestCases.APIViewTestCase): self.add_permissions('ipam.delete_vlan') url = reverse('ipam-api:vlan-detail', kwargs={'pk': vlan.pk}) - with disable_warnings('netbox.api.views.ModelViewSet'): + with disable_logging(level=logging.WARNING): response = self.client.delete(url, **self.header) self.assertHttpStatus(response, status.HTTP_409_CONFLICT) diff --git a/netbox/utilities/tests/test_api.py b/netbox/utilities/tests/test_api.py index 2c3ba0566..b6e5e1360 100644 --- a/netbox/utilities/tests/test_api.py +++ b/netbox/utilities/tests/test_api.py @@ -1,5 +1,6 @@ from django.test import Client, TestCase, override_settings from django.urls import reverse +from drf_spectacular.drainage import GENERATOR_STATS from rest_framework import status from core.models import ObjectType @@ -264,5 +265,6 @@ class APIDocsTestCase(TestCase): self.assertEqual(response.status_code, 200) url = reverse('schema') - response = self.client.get(url) + with GENERATOR_STATS.silence(): # Suppress schema generator warnings + response = self.client.get(url) self.assertEqual(response.status_code, 200) diff --git a/netbox/virtualization/tests/test_api.py b/netbox/virtualization/tests/test_api.py index dfa8309a0..e07f4dc06 100644 --- a/netbox/virtualization/tests/test_api.py +++ b/netbox/virtualization/tests/test_api.py @@ -1,3 +1,5 @@ +import logging + from django.test import tag from django.urls import reverse from netaddr import IPNetwork @@ -10,7 +12,9 @@ from extras.choices import CustomFieldTypeChoices from extras.models import ConfigTemplate, CustomField from ipam.choices import VLANQinQRoleChoices from ipam.models import Prefix, VLAN, VRF -from utilities.testing import APITestCase, APIViewTestCases, create_test_device, create_test_virtualmachine +from utilities.testing import ( + APITestCase, APIViewTestCases, create_test_device, create_test_virtualmachine, disable_logging, +) from virtualization.choices import * from virtualization.models import * @@ -402,7 +406,8 @@ class VMInterfaceTest(APIViewTestCases.APIViewTestCase): # Attempt to delete only the parent interface url = self._get_detail_url(interface1) - self.client.delete(url, **self.header) + with disable_logging(level=logging.WARNING): + self.client.delete(url, **self.header) self.assertEqual(virtual_machine.interfaces.count(), 4) # Parent was not deleted # Attempt to bulk delete parent & child together From b3d318cbe1a4bef1673673df421f29e2862d1b31 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 05:02:08 +0000 Subject: [PATCH 16/30] Update source translation strings --- netbox/translations/en/LC_MESSAGES/django.po | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index 349d4a68a..95ba5438d 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-05-20 05:01+0000\n" +"POT-Creation-Date: 2025-05-23 05:01+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -14065,7 +14065,11 @@ msgstr "" msgid "Error rendering template" msgstr "" -#: netbox/templates/extras/object_render_config.html:74 +#: netbox/templates/extras/object_render_config.html:73 +msgid "Template output is empty" +msgstr "" + +#: netbox/templates/extras/object_render_config.html:78 msgid "No configuration template has been assigned." msgstr "" From d7672ab2605c5232f07410752a2c55ab46bd9965 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Fri, 23 May 2025 16:34:22 -0500 Subject: [PATCH 17/30] Fixes #19490: restores nesting behavior of DataSource-based ConfigTemplates The ability to render nested templates was accidentally removed with the implementation of #17653, which normalized the behavior of various Jinja2 template rendering actions. This fix restores that behavior while retaining the normalized behavior. This fix also includes regression tests to ensure this behavior is not removed accidentally again in the future. --- netbox/extras/models/mixins.py | 2 +- netbox/extras/tests/test_models.py | 78 +++++++++++++++++++++++++++--- netbox/utilities/jinja2.py | 20 +++++++- 3 files changed, 91 insertions(+), 9 deletions(-) diff --git a/netbox/extras/models/mixins.py b/netbox/extras/models/mixins.py index 3a7273f93..eb017302a 100644 --- a/netbox/extras/models/mixins.py +++ b/netbox/extras/models/mixins.py @@ -131,7 +131,7 @@ class RenderTemplateMixin(models.Model): """ context = self.get_context(context=context, queryset=queryset) env_params = self.environment_params or {} - output = render_jinja2(self.template_code, context, env_params) + output = render_jinja2(self.template_code, context, env_params, getattr(self, 'data_file', None)) # Replace CRLF-style line terminators output = output.replace('\r\n', '\n') diff --git a/netbox/extras/tests/test_models.py b/netbox/extras/tests/test_models.py index 089e47c02..6b718569c 100644 --- a/netbox/extras/tests/test_models.py +++ b/netbox/extras/tests/test_models.py @@ -1,9 +1,12 @@ -from django.forms import ValidationError -from django.test import TestCase +import tempfile +from pathlib import Path -from core.models import ObjectType +from django.forms import ValidationError +from django.test import tag, TestCase + +from core.models import DataSource, ObjectType from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site, SiteGroup -from extras.models import ConfigContext, Tag +from extras.models import ConfigContext, ConfigTemplate, Tag from tenancy.models import Tenant, TenantGroup from utilities.exceptions import AbortRequest from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -33,8 +36,8 @@ class TagTest(TestCase): ] site = Site.objects.create(name='Site 1') - for tag in tags: - site.tags.add(tag) + for _tag in tags: + site.tags.add(_tag) site.save() site = Site.objects.first() @@ -540,3 +543,66 @@ class ConfigContextTest(TestCase): device.local_context_data = 'foo' with self.assertRaises(ValidationError): device.clean() + + +class ConfigTemplateTest(TestCase): + """ + TODO: These test cases deal with the weighting, ordering, and deep merge logic of config context data. + """ + MAIN_TEMPLATE = """ + {%- include 'base.j2' %} + """.strip() + BASE_TEMPLATE = """ + Hi + """.strip() + + @classmethod + def _create_template_file(cls, templates_dir, file_name, content): + template_file_name = file_name + if not template_file_name.endswith('j2'): + template_file_name += '.j2' + temp_file_path = templates_dir / template_file_name + + with open(temp_file_path, 'w') as f: + f.write(content) + + @classmethod + def setUpTestData(cls): + temp_dir = tempfile.TemporaryDirectory() + templates_dir = Path(temp_dir.name) / "templates" + templates_dir.mkdir(parents=True, exist_ok=True) + + cls._create_template_file(templates_dir, 'base.j2', cls.BASE_TEMPLATE) + cls._create_template_file(templates_dir, 'main.j2', cls.MAIN_TEMPLATE) + + data_source = DataSource( + name="Test DataSource", + type="local", + source_url=str(templates_dir), + ) + data_source.save() + data_source.sync() + + base_config_template = ConfigTemplate( + name="BaseTemplate", + data_file=data_source.datafiles.filter(path__endswith='base.j2').first() + ) + base_config_template.clean() + base_config_template.save() + cls.base_config_template = base_config_template + + main_config_template = ConfigTemplate( + name="MainTemplate", + data_file=data_source.datafiles.filter(path__endswith='main.j2').first() + ) + main_config_template.clean() + main_config_template.save() + cls.main_config_template = main_config_template + + @tag('regression') + def test_config_template_with_data_source(self): + self.assertEqual(self.BASE_TEMPLATE, self.base_config_template.render({})) + + @tag('regression') + def test_config_template_with_data_source_nested_templates(self): + self.assertEqual(self.BASE_TEMPLATE, self.main_config_template.render({})) diff --git a/netbox/utilities/jinja2.py b/netbox/utilities/jinja2.py index 37b3b2dfb..362bc2393 100644 --- a/netbox/utilities/jinja2.py +++ b/netbox/utilities/jinja2.py @@ -49,11 +49,27 @@ class DataFileLoader(BaseLoader): # Utility functions # -def render_jinja2(template_code, context, environment_params=None): +def render_jinja2(template_code, context, environment_params=None, data_file=None): """ Render a Jinja2 template with the provided context. Return the rendered content. """ environment_params = environment_params or {} + + if 'loader' not in environment_params: + if data_file: + loader = DataFileLoader(data_file.source) + loader.cache_templates({ + data_file.path: template_code + }) + else: + loader = BaseLoader() + environment_params['loader'] = loader + environment = SandboxedEnvironment(**environment_params) environment.filters.update(get_config().JINJA2_FILTERS) - return environment.from_string(source=template_code).render(**context) + + if data_file: + template = environment.get_template(data_file.path) + else: + template = environment.from_string(source=template_code) + return template.render(**context) From a97b438b7ec4c3a7c28fbd8d5d989541463d8c20 Mon Sep 17 00:00:00 2001 From: Alexander Haase Date: Tue, 27 May 2025 19:07:48 +0200 Subject: [PATCH 18/30] Fixes #19530: Overhaul documentation for plugin views (#19530) --- docs/plugins/development/views.md | 53 +++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/docs/plugins/development/views.md b/docs/plugins/development/views.md index 43cc0ce82..d5d376db8 100644 --- a/docs/plugins/development/views.md +++ b/docs/plugins/development/views.md @@ -1,6 +1,6 @@ # Views -## Writing Views +## Writing Basic Views If your plugin will provide its own page or pages within the NetBox web UI, you'll need to define views. A view is a piece of business logic which performs an action and/or renders a page when a request is made to a particular URL. HTML content is rendered using a [template](./templates.md). Views are typically defined in `views.py`, and URL patterns in `urls.py`. @@ -47,9 +47,13 @@ A URL pattern has three components: This makes our view accessible at the URL `/plugins/animal-sounds/random/`. (Remember, our `AnimalSoundsConfig` class sets our plugin's base URL to `animal-sounds`.) Viewing this URL should show the base NetBox template with our custom content inside it. +## NetBox Model Views + +NetBox provides several generic view classes and additional helper functions, to simplify the implementation of plugin logic. These are recommended to be used whenever possible to keep the maintenance overhead of plugins low. + ### View Classes -NetBox provides several generic view classes (documented below) to facilitate common operations, such as creating, viewing, modifying, and deleting objects. Plugins can subclass these views for their own use. +Generic view classes (documented below) facilitate common operations, such as creating, viewing, modifying, and deleting objects. Plugins can subclass these views for their own use. | View Class | Description | |----------------------|--------------------------------------------------------| @@ -65,18 +69,51 @@ NetBox provides several generic view classes (documented below) to facilitate co !!! warning Please note that only the classes which appear in this documentation are currently supported. Although other classes may be present within the `views.generic` module, they are not yet supported for use by plugins. -#### Example Usage +### URL registration + +The NetBox URL registration process has two parts: + +1. View classes can be decorated with `@register_model_view()`. This registers a new URL for the model. +2. All of a model's URLs can be included in `urls.py` using the `get_model_urls()` function. This call is usually required twice: once to import general views for the model and again to import model detail views tied to the object's primary key. + +::: utilities.views.register_model_view + +!!! note "Changed in NetBox v4.2" + In NetBox v4.2, the `register_model_view()` function was extended to support the registration of list views by passing `detail=False`. + +::: utilities.urls.get_model_urls + +!!! note "Changed in NetBox v4.2" + In NetBox v4.2, the `get_model_urls()` function was extended to support retrieving registered general model views (e.g. for listing objects) by passing `detail=False`. + +### Example Usage ```python # views.py from netbox.views.generic import ObjectEditView +from utilities.views import register_model_view from .models import Thing +@register_model_view(Thing, name='add', detail=False) +@register_model_view(Thing, name='edit') class ThingEditView(ObjectEditView): queryset = Thing.objects.all() template_name = 'myplugin/thing.html' ... ``` + +```python +# urls.py +from django.urls import include, path +from utilities.urls import get_model_urls + +urlpatterns = [ + path('thing/', include(get_model_urls('myplugin', 'thing', detail=False))), + path('thing//', include(get_model_urls('myplugin', 'thing'))), + ... +] +``` + ## Object Views Below are the class definitions for NetBox's object views. These views handle CRUD actions for individual objects. The view, add/edit, and delete views each inherit from `BaseObjectView`, which is not intended to be used directly. @@ -143,6 +180,9 @@ Below are the class definitions for NetBox's multi-object views. These views han These views are provided to enable or enhance certain NetBox model features, such as change logging or journaling. These typically do not need to be subclassed: They can be used directly e.g. in a URL path. +!!! note + These feature views are automatically registered for all models that implement the respective feature. There is usually no need to override them. However, if that's the case, the URL must be registered manually in `urls.py` instead of using the `register_model_view()` function or decorator. + ::: netbox.views.generic.ObjectChangeLogView options: members: @@ -157,7 +197,7 @@ These views are provided to enable or enhance certain NetBox model features, suc ### Additional Tabs -Plugins can "attach" a custom view to a core NetBox model by registering it with `register_model_view()`. To include a tab for this view within the NetBox UI, declare a TabView instance named `tab`, and add it to the template context dict: +Plugins can "attach" a custom view to a NetBox model by registering it with `register_model_view()`. To include a tab for this view within the NetBox UI, declare a TabView instance named `tab`, and add it to the template context dict: ```python from dcim.models import Site @@ -185,11 +225,6 @@ class MyView(generic.ObjectView): ) ``` -!!! note "Changed in NetBox v4.2" - The `register_model_view()` function was extended in NetBox v4.2 to support registration of list views by passing `detail=False`. - -::: utilities.views.register_model_view - ::: utilities.views.ViewTab ### Extra Template Content From cc099e86e103a6c3b74323fd644b18f47de9e148 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Tue, 27 May 2025 12:32:36 -0500 Subject: [PATCH 19/30] Fixes #19520: restores ability to set Prefix.scope via API (#19588) --- netbox/dcim/models/mixins.py | 2 +- netbox/ipam/tests/test_api.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/models/mixins.py b/netbox/dcim/models/mixins.py index 127dfb9e5..e9484264c 100644 --- a/netbox/dcim/models/mixins.py +++ b/netbox/dcim/models/mixins.py @@ -85,7 +85,7 @@ class CachedScopeMixin(models.Model): abstract = True def clean(self): - if self.scope_type and not self.scope: + if self.scope_type and not (self.scope or self.scope_id): scope_type = self.scope_type.model_class() raise ValidationError({ 'scope': _( diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index 6255aaf86..a7562a53b 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -1,6 +1,7 @@ import json import logging +from django.test import tag from django.urls import reverse from netaddr import IPNetwork from rest_framework import status @@ -383,6 +384,18 @@ class PrefixTest(APIViewTestCases.APIViewTestCase): ) Prefix.objects.bulk_create(prefixes) + @tag('regression') + def test_clean_validates_scope(self): + prefix = Prefix.objects.first() + site = Site.objects.create(name='Test Site', slug='test-site') + + data = {'scope_type': 'dcim.site', 'scope_id': site.id} + url = reverse('ipam-api:prefix-detail', kwargs={'pk': prefix.pk}) + self.add_permissions('ipam.change_prefix') + + response = self.client.patch(url, data, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + def test_list_available_prefixes(self): """ Test retrieval of all available prefixes within a parent prefix. From b62f2347c59f0791de1c5145cc499057905325de Mon Sep 17 00:00:00 2001 From: mr1716 Date: Fri, 30 May 2025 14:57:22 -0400 Subject: [PATCH 20/30] Closes #19611: Update index.md To Spell Acronym First Time It's Found (#19614) * Update index.md To Spell Acronym First Time It's Found * Update index.md to make lower case --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index a79ab03b4..1494de5f3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,7 +9,7 @@ NetBox is the leading solution for modeling and documenting modern networks. By ## :material-server-network: Built for Networks -Unlike general-purpose CMDBs, NetBox has curated a data model which caters specifically to the needs of network engineers and operators. It delivers a wide assortment of object types carefully crafted to best serve the needs of infrastructure design and documentation. These cover all facets of network technology, from IP address managements to cabling to overlays and more: +Unlike general-purpose configuration management databases (CMDBs), NetBox has curated a data model which caters specifically to the needs of network engineers and operators. It delivers a wide assortment of object types carefully crafted to best serve the needs of infrastructure design and documentation. These cover all facets of network technology, from IP address managements to cabling to overlays and more: * Hierarchical regions, sites, and locations * Racks, devices, and device components From 357ae44cde6085e47790ffd248b92c9211171c69 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 2 Jun 2025 15:14:11 -0400 Subject: [PATCH 21/30] Fixes #19599: Prevent exception when sorting user's recent activity --- netbox/account/views.py | 1 + netbox/templates/account/profile.html | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/netbox/account/views.py b/netbox/account/views.py index a2f21b44e..a309232b9 100644 --- a/netbox/account/views.py +++ b/netbox/account/views.py @@ -197,6 +197,7 @@ class ProfileView(LoginRequiredMixin, View): 'changed_object_type' )[:20] changelog_table = ObjectChangeTable(changelog) + changelog_table.orderable = False changelog_table.configure(request) return render(request, self.template_name, { diff --git a/netbox/templates/account/profile.html b/netbox/templates/account/profile.html index 20f8ad537..e513a6e85 100644 --- a/netbox/templates/account/profile.html +++ b/netbox/templates/account/profile.html @@ -65,7 +65,14 @@
-

{% trans "Recent Activity" %}

+

+ {% trans "Recent Activity" %} + +

{% render_table changelog_table 'inc/table.html' %}
From e5e7a66cb9f2a9bf29e442d57eabf62b487f5b86 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 3 Jun 2025 08:58:16 -0400 Subject: [PATCH 22/30] Apply fix to user view as well --- netbox/account/views.py | 6 +----- netbox/templates/account/profile.html | 18 ++---------------- netbox/templates/users/inc/user_activity.html | 16 ++++++++++++++++ netbox/templates/users/user.html | 11 ++--------- netbox/users/views.py | 3 ++- 5 files changed, 23 insertions(+), 31 deletions(-) create mode 100644 netbox/templates/users/inc/user_activity.html diff --git a/netbox/account/views.py b/netbox/account/views.py index a309232b9..f5ef534ce 100644 --- a/netbox/account/views.py +++ b/netbox/account/views.py @@ -191,11 +191,7 @@ class ProfileView(LoginRequiredMixin, View): def get(self, request): # Compile changelog table - changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter( - user=request.user - ).prefetch_related( - 'changed_object_type' - )[:20] + changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(user=request.user)[:20] changelog_table = ObjectChangeTable(changelog) changelog_table.orderable = False changelog_table.configure(request) diff --git a/netbox/templates/account/profile.html b/netbox/templates/account/profile.html index e513a6e85..442cce9ba 100644 --- a/netbox/templates/account/profile.html +++ b/netbox/templates/account/profile.html @@ -1,12 +1,10 @@ {% extends 'account/base.html' %} -{% load helpers %} -{% load render_table from django_tables2 %} {% load i18n %} {% block title %}{% trans "User Profile" %}{% endblock %} {% block content %} -
+

{% trans "Account Details" %}

@@ -64,19 +62,7 @@ {% if perms.core.view_objectchange %}
-
-

- {% trans "Recent Activity" %} - -

-
- {% render_table changelog_table 'inc/table.html' %} -
-
+ {% include 'users/inc/user_activity.html' with user=user table=changelog_table %}
{% endif %} diff --git a/netbox/templates/users/inc/user_activity.html b/netbox/templates/users/inc/user_activity.html new file mode 100644 index 000000000..74d6500ab --- /dev/null +++ b/netbox/templates/users/inc/user_activity.html @@ -0,0 +1,16 @@ +{% load i18n %} +{% load render_table from django_tables2 %} + +
+

+ {% trans "Recent Activity" %} + +

+
+ {% render_table table 'inc/table.html' %} +
+
diff --git a/netbox/templates/users/user.html b/netbox/templates/users/user.html index 967cc0537..84e4cac68 100644 --- a/netbox/templates/users/user.html +++ b/netbox/templates/users/user.html @@ -1,14 +1,12 @@ {% extends 'generic/object.html' %} {% load i18n %} -{% load helpers %} -{% load render_table from django_tables2 %} {% block title %}{% trans "User" %} {{ object.username }}{% endblock %} {% block subtitle %}{% endblock %} {% block content %} -
+

{% trans "User" %}

@@ -74,12 +72,7 @@ {% if perms.core.view_objectchange %}
-
-

{% trans "Recent Activity" %}

-
- {% render_table changelog_table 'inc/table.html' %} -
-
+ {% include 'users/inc/user_activity.html' with user=object table=changelog_table %}
{% endif %} diff --git a/netbox/users/views.py b/netbox/users/views.py index 16dacaa46..099bbcf87 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -75,8 +75,9 @@ class UserView(generic.ObjectView): template_name = 'users/user.html' def get_extra_context(self, request, instance): - changelog = ObjectChange.objects.restrict(request.user, 'view').filter(user=instance)[:20] + changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(user=instance)[:20] changelog_table = ObjectChangeTable(changelog) + changelog_table.orderable = False changelog_table.configure(request) return { From b1cbdbe07943dee75b2e819766ab061d3b8e1c45 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Tue, 3 Jun 2025 06:03:44 -0700 Subject: [PATCH 23/30] 19623 show description on provider account detail view (#19629) * 19623 show description on provider account detail view * Fix indentation --------- Co-authored-by: Jeremy Stretch --- netbox/templates/circuits/provideraccount.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/netbox/templates/circuits/provideraccount.html b/netbox/templates/circuits/provideraccount.html index 8715dfe1e..41cf8c033 100644 --- a/netbox/templates/circuits/provideraccount.html +++ b/netbox/templates/circuits/provideraccount.html @@ -28,6 +28,10 @@ {% trans "Name" %} {{ object.name|placeholder }} + + {% trans "Description" %} + {{ object.description|placeholder }} +
{% include 'inc/panels/tags.html' %} From f45b671fc91bc2e61c34e357da54e822d90ad576 Mon Sep 17 00:00:00 2001 From: mr1716 Date: Tue, 3 Jun 2025 09:13:10 -0400 Subject: [PATCH 24/30] #19619 update documentation for consistency (#19620) * Update system.md For Capitalization Consistency * Update security.md For Consistency * Update system.md To Improve Consistency * Update security.md for Consistency * Update docs/configuration/security.md * Update docs/configuration/system.md --------- Co-authored-by: Jeremy Stretch --- docs/configuration/security.md | 14 +++++++------- docs/configuration/system.md | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/configuration/security.md b/docs/configuration/security.md index 676c1a336..a647a65b8 100644 --- a/docs/configuration/security.md +++ b/docs/configuration/security.md @@ -52,7 +52,7 @@ Although it is not recommended, the default validation rules can be disabled by Default: `False` -If True, cross-origin resource sharing (CORS) requests will be accepted from all origins. If False, a whitelist will be used (see below). +If `True`, cross-origin resource sharing (CORS) requests will be accepted from all origins. If False, a whitelist will be used (see below). --- @@ -84,7 +84,7 @@ The name of the cookie to use for the cross-site request forgery (CSRF) authenti Default: `False` -If true, the cookie employed for cross-site request forgery (CSRF) protection will be marked as secure, meaning that it can only be sent across an HTTPS connection. +If `True`, the cookie employed for cross-site request forgery (CSRF) protection will be marked as secure, meaning that it can only be sent across an HTTPS connection. --- @@ -164,7 +164,7 @@ EXEMPT_VIEW_PERMISSIONS = ['*'] Default: `False` -If true, the lifetime of a user's authentication session will be automatically reset upon each valid request. For example, if [`LOGIN_TIMEOUT`](#login_timeout) is configured to 14 days (the default), and a user whose session is due to expire in five days makes a NetBox request (with a valid session cookie), the session's lifetime will be reset to 14 days. +If `True`, the lifetime of a user's authentication session will be automatically reset upon each valid request. For example, if [`LOGIN_TIMEOUT`](#login_timeout) is configured to 14 days (the default), and a user whose session is due to expire in five days makes a NetBox request (with a valid session cookie), the session's lifetime will be reset to 14 days. Note that enabling this setting causes NetBox to update a user's session in the database (or file, as configured per [`SESSION_FILE_PATH`](#session_file_path)) with each request, which may introduce significant overhead in very active environments. It also permits an active user to remain authenticated to NetBox indefinitely. @@ -212,7 +212,7 @@ The view name or URL to which a user is redirected after logging out. Default: `False` -If true, the `includeSubDomains` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to apply the HSTS policy to all subdomains of the current domain. +If `True`, the `includeSubDomains` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to apply the HSTS policy to all subdomains of the current domain. --- @@ -220,7 +220,7 @@ If true, the `includeSubDomains` directive will be included in the HTTP Strict T Default: `False` -If true, the `preload` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to preload the site in HTTPS. Browsers that use the HSTS preload list will force the site to be accessed via HTTPS even if the user types HTTP in the address bar. +If `True`, the `preload` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to preload the site in HTTPS. Browsers that use the HSTS preload list will force the site to be accessed via HTTPS even if the user types HTTP in the address bar. --- @@ -236,7 +236,7 @@ If set to a non-zero integer value, the SecurityMiddleware sets the HTTP Strict Default: `False` -If true, all non-HTTPS requests will be automatically redirected to use HTTPS. +If `True`, all non-HTTPS requests will be automatically redirected to use HTTPS. !!! warning Ensure that your frontend HTTP daemon has been configured to forward the HTTP scheme correctly before enabling this option. An incorrectly configured frontend may result in a looping redirect. @@ -255,7 +255,7 @@ The name used for the session cookie. See the [Django documentation](https://doc Default: `False` -If true, the cookie employed for session authentication will be marked as secure, meaning that it can only be sent across an HTTPS connection. +If `True`, the cookie employed for session authentication will be marked as secure, meaning that it can only be sent across an HTTPS connection. --- diff --git a/docs/configuration/system.md b/docs/configuration/system.md index fe01e40b1..20143276c 100644 --- a/docs/configuration/system.md +++ b/docs/configuration/system.md @@ -95,7 +95,7 @@ Default: `('127.0.0.1', '::1')` A list of IP addresses recognized as internal to the system, used to control the display of debugging output. For example, the debugging toolbar will be viewable only when a client is accessing NetBox from one of the listed IP -addresses (and [`DEBUG`](./development.md#debug) is true). +addresses (and [`DEBUG`](./development.md#debug) is `True`). --- @@ -103,7 +103,7 @@ addresses (and [`DEBUG`](./development.md#debug) is true). Default: `False` -Set this configuration parameter to True for NetBox deployments which do not have Internet access. This will disable miscellaneous functionality which depends on access to the Internet. +Set this configuration parameter to `True` for NetBox deployments which do not have Internet access. This will disable miscellaneous functionality which depends on access to the Internet. !!! note If Internet access is available via a proxy, set [`HTTP_PROXIES`](#http_proxies) instead. @@ -114,7 +114,7 @@ Set this configuration parameter to True for NetBox deployments which do not hav Default: `{}` -A dictionary of custom jinja2 filters with the key being the filter name and the value being a callable. For more information see the [Jinja2 documentation](https://jinja.palletsprojects.com/en/3.1.x/api/#custom-filters). For example: +A dictionary of custom Jinja2 filters with the key being the filter name and the value being a callable. For more information see the [Jinja2 documentation](https://jinja.palletsprojects.com/en/3.1.x/api/#custom-filters). For example: ```python def uppercase(x): From 77f0eeb7bf697ab3bf29ab2e22186b69c281ed62 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 3 Jun 2025 10:34:39 -0400 Subject: [PATCH 25/30] Fixes #19587: Occupied filter should match on interfaces terminating a wireless link (#19631) --- netbox/dcim/filtersets.py | 15 +++++++++++++++ netbox/dcim/tests/test_filtersets.py | 13 ++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index a31cf136d..7f1493557 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -2012,6 +2012,21 @@ class InterfaceFilterSet( 'wireless': queryset.filter(type__in=WIRELESS_IFACE_TYPES), }.get(value, queryset.none()) + # Override the method on CabledObjectFilterSet to also check for wireless links + def filter_occupied(self, queryset, name, value): + if value: + return queryset.filter( + Q(cable__isnull=False) | + Q(wireless_link__isnull=False) | + Q(mark_connected=True) + ) + else: + return queryset.filter( + cable__isnull=True, + wireless_link__isnull=True, + mark_connected=False + ) + class FrontPortFilterSet( ModularDeviceComponentFilterSet, diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index ba8d4203d..2ae178653 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -12,6 +12,7 @@ from users.models import User from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine from virtualization.models import Cluster, ClusterType, ClusterGroup, VMInterface, VirtualMachine from wireless.choices import WirelessChannelChoices, WirelessRoleChoices +from wireless.models import WirelessLink class DeviceComponentFilterSetTests: @@ -4496,7 +4497,9 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil # Cables Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[5]]).save() Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[6]]).save() - # Third pair is not connected + + # Wireless links + WirelessLink(interface_a=interfaces[7], interface_b=interfaces[8]).save() def test_name(self): params = {'name': ['Interface 1', 'Interface 2']} @@ -4684,15 +4687,15 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil def test_occupied(self): params = {'occupied': True} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) params = {'occupied': False} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) def test_connected(self): params = {'connected': True} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) params = {'connected': False} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5) + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) def test_kind(self): params = {'kind': 'physical'} From 065511fca23a9b49b2936a6b3333b908564fd980 Mon Sep 17 00:00:00 2001 From: Marcus Weiner Date: Tue, 3 Jun 2025 18:15:55 +0200 Subject: [PATCH 26/30] Allow filtering IP addresses by family in GraphQL (#19621) --- netbox/ipam/graphql/filters.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/netbox/ipam/graphql/filters.py b/netbox/ipam/graphql/filters.py index 7421a935a..53096af38 100644 --- a/netbox/ipam/graphql/filters.py +++ b/netbox/ipam/graphql/filters.py @@ -159,6 +159,14 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter return Q() return q + @strawberry_django.filter_field() + def family( + self, + value: Annotated['IPAddressFamilyEnum', strawberry.lazy('ipam.graphql.enums')], + prefix, + ) -> Q: + return Q(**{f"{prefix}address__family": value.value}) + @strawberry_django.filter_type(models.IPRange, lookups=True) class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin): From ecb86567233e1ac4d4379590fdc7b6b45315767a Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Jun 2025 05:02:13 +0000 Subject: [PATCH 27/30] Update source translation strings --- netbox/translations/en/LC_MESSAGES/django.po | 65 ++++++++++---------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index 95ba5438d..162a0ce10 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-05-23 05:01+0000\n" +"POT-Creation-Date: 2025-06-04 05:01+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -165,8 +165,8 @@ msgstr "" #: netbox/dcim/filtersets.py:215 netbox/dcim/filtersets.py:336 #: netbox/dcim/filtersets.py:467 netbox/dcim/filtersets.py:1075 #: netbox/dcim/filtersets.py:1397 netbox/dcim/filtersets.py:1495 -#: netbox/dcim/filtersets.py:2160 netbox/dcim/filtersets.py:2403 -#: netbox/dcim/filtersets.py:2461 netbox/ipam/filtersets.py:954 +#: netbox/dcim/filtersets.py:2175 netbox/dcim/filtersets.py:2418 +#: netbox/dcim/filtersets.py:2476 netbox/ipam/filtersets.py:954 #: netbox/virtualization/filtersets.py:139 netbox/vpn/filtersets.py:361 msgid "Region (ID)" msgstr "" @@ -177,8 +177,8 @@ msgstr "" #: netbox/dcim/filtersets.py:222 netbox/dcim/filtersets.py:343 #: netbox/dcim/filtersets.py:474 netbox/dcim/filtersets.py:1082 #: netbox/dcim/filtersets.py:1404 netbox/dcim/filtersets.py:1502 -#: netbox/dcim/filtersets.py:2167 netbox/dcim/filtersets.py:2410 -#: netbox/dcim/filtersets.py:2468 netbox/extras/filtersets.py:602 +#: netbox/dcim/filtersets.py:2182 netbox/dcim/filtersets.py:2425 +#: netbox/dcim/filtersets.py:2483 netbox/extras/filtersets.py:602 #: netbox/ipam/filtersets.py:961 netbox/virtualization/filtersets.py:146 #: netbox/vpn/filtersets.py:356 msgid "Region (slug)" @@ -189,8 +189,8 @@ msgstr "" #: netbox/dcim/filtersets.py:131 netbox/dcim/filtersets.py:228 #: netbox/dcim/filtersets.py:349 netbox/dcim/filtersets.py:480 #: netbox/dcim/filtersets.py:1088 netbox/dcim/filtersets.py:1410 -#: netbox/dcim/filtersets.py:1508 netbox/dcim/filtersets.py:2173 -#: netbox/dcim/filtersets.py:2416 netbox/dcim/filtersets.py:2474 +#: netbox/dcim/filtersets.py:1508 netbox/dcim/filtersets.py:2188 +#: netbox/dcim/filtersets.py:2431 netbox/dcim/filtersets.py:2489 #: netbox/ipam/filtersets.py:239 netbox/ipam/filtersets.py:967 #: netbox/virtualization/filtersets.py:152 msgid "Site group (ID)" @@ -201,8 +201,8 @@ msgstr "" #: netbox/dcim/filtersets.py:138 netbox/dcim/filtersets.py:235 #: netbox/dcim/filtersets.py:356 netbox/dcim/filtersets.py:487 #: netbox/dcim/filtersets.py:1095 netbox/dcim/filtersets.py:1417 -#: netbox/dcim/filtersets.py:1515 netbox/dcim/filtersets.py:2180 -#: netbox/dcim/filtersets.py:2423 netbox/dcim/filtersets.py:2481 +#: netbox/dcim/filtersets.py:1515 netbox/dcim/filtersets.py:2195 +#: netbox/dcim/filtersets.py:2438 netbox/dcim/filtersets.py:2496 #: netbox/extras/filtersets.py:608 netbox/ipam/filtersets.py:246 #: netbox/ipam/filtersets.py:974 netbox/virtualization/filtersets.py:159 msgid "Site group (slug)" @@ -318,8 +318,8 @@ msgstr "" #: netbox/dcim/base_filtersets.py:47 netbox/dcim/filtersets.py:239 #: netbox/dcim/filtersets.py:360 netbox/dcim/filtersets.py:455 #: netbox/dcim/filtersets.py:1099 netbox/dcim/filtersets.py:1422 -#: netbox/dcim/filtersets.py:1520 netbox/dcim/filtersets.py:2185 -#: netbox/dcim/filtersets.py:2427 netbox/dcim/filtersets.py:2486 +#: netbox/dcim/filtersets.py:1520 netbox/dcim/filtersets.py:2200 +#: netbox/dcim/filtersets.py:2442 netbox/dcim/filtersets.py:2501 #: netbox/ipam/filtersets.py:251 netbox/ipam/filtersets.py:978 #: netbox/virtualization/filtersets.py:163 netbox/vpn/filtersets.py:371 msgid "Site (ID)" @@ -329,7 +329,7 @@ msgstr "" #: netbox/dcim/base_filtersets.py:59 netbox/dcim/filtersets.py:261 #: netbox/dcim/filtersets.py:372 netbox/dcim/filtersets.py:493 #: netbox/dcim/filtersets.py:1111 netbox/dcim/filtersets.py:1433 -#: netbox/dcim/filtersets.py:1531 netbox/dcim/filtersets.py:2439 +#: netbox/dcim/filtersets.py:1531 netbox/dcim/filtersets.py:2454 msgid "Location (ID)" msgstr "" @@ -341,7 +341,7 @@ msgstr "" #: netbox/circuits/filtersets.py:537 netbox/core/filtersets.py:81 #: netbox/core/filtersets.py:140 netbox/core/filtersets.py:177 #: netbox/dcim/filtersets.py:780 netbox/dcim/filtersets.py:1489 -#: netbox/dcim/filtersets.py:2534 netbox/extras/filtersets.py:45 +#: netbox/dcim/filtersets.py:2549 netbox/extras/filtersets.py:45 #: netbox/extras/filtersets.py:67 netbox/extras/filtersets.py:96 #: netbox/extras/filtersets.py:136 netbox/extras/filtersets.py:185 #: netbox/extras/filtersets.py:213 netbox/extras/filtersets.py:243 @@ -503,6 +503,7 @@ msgstr "" #: netbox/templates/circuits/circuittype.html:26 #: netbox/templates/circuits/inc/circuit_termination_fields.html:83 #: netbox/templates/circuits/provider.html:33 +#: netbox/templates/circuits/provideraccount.html:32 #: netbox/templates/circuits/providernetwork.html:32 #: netbox/templates/circuits/virtualcircuit.html:56 #: netbox/templates/circuits/virtualcircuittermination.html:68 @@ -1827,7 +1828,7 @@ msgstr "" #: netbox/netbox/navigation/menu.py:275 netbox/netbox/navigation/menu.py:279 #: netbox/netbox/navigation/menu.py:281 #: netbox/templates/circuits/provider.html:57 -#: netbox/templates/circuits/provideraccount.html:44 +#: netbox/templates/circuits/provideraccount.html:48 #: netbox/templates/circuits/providernetwork.html:50 msgid "Circuits" msgstr "" @@ -3440,7 +3441,7 @@ msgstr "" #: netbox/dcim/filtersets.py:542 netbox/dcim/filtersets.py:707 #: netbox/dcim/filtersets.py:911 netbox/dcim/filtersets.py:985 #: netbox/dcim/filtersets.py:1025 netbox/dcim/filtersets.py:1368 -#: netbox/dcim/filtersets.py:2093 +#: netbox/dcim/filtersets.py:2108 msgid "Manufacturer (ID)" msgstr "" @@ -3448,7 +3449,7 @@ msgstr "" #: netbox/dcim/filtersets.py:548 netbox/dcim/filtersets.py:713 #: netbox/dcim/filtersets.py:917 netbox/dcim/filtersets.py:991 #: netbox/dcim/filtersets.py:1031 netbox/dcim/filtersets.py:1374 -#: netbox/dcim/filtersets.py:2099 +#: netbox/dcim/filtersets.py:2114 msgid "Manufacturer (slug)" msgstr "" @@ -3461,14 +3462,14 @@ msgid "Rack type (ID)" msgstr "" #: netbox/dcim/filtersets.py:414 netbox/dcim/filtersets.py:921 -#: netbox/dcim/filtersets.py:1047 netbox/dcim/filtersets.py:2103 +#: netbox/dcim/filtersets.py:1047 netbox/dcim/filtersets.py:2118 #: netbox/ipam/filtersets.py:376 netbox/ipam/filtersets.py:488 #: netbox/ipam/filtersets.py:998 netbox/virtualization/filtersets.py:177 msgid "Role (ID)" msgstr "" #: netbox/dcim/filtersets.py:420 netbox/dcim/filtersets.py:927 -#: netbox/dcim/filtersets.py:1054 netbox/dcim/filtersets.py:2109 +#: netbox/dcim/filtersets.py:1054 netbox/dcim/filtersets.py:2124 #: netbox/extras/filtersets.py:651 netbox/ipam/filtersets.py:382 #: netbox/ipam/filtersets.py:494 netbox/ipam/filtersets.py:1004 #: netbox/virtualization/filtersets.py:184 @@ -3477,7 +3478,7 @@ msgstr "" #: netbox/dcim/filtersets.py:450 netbox/dcim/filtersets.py:1123 #: netbox/dcim/filtersets.py:1444 netbox/dcim/filtersets.py:1542 -#: netbox/dcim/filtersets.py:2501 +#: netbox/dcim/filtersets.py:2516 msgid "Rack (ID)" msgstr "" @@ -3575,7 +3576,7 @@ msgstr "" msgid "Power port (ID)" msgstr "" -#: netbox/dcim/filtersets.py:907 netbox/dcim/filtersets.py:2089 +#: netbox/dcim/filtersets.py:907 netbox/dcim/filtersets.py:2104 msgid "Parent inventory item (ID)" msgstr "" @@ -3610,8 +3611,8 @@ msgid "Platform (slug)" msgstr "" #: netbox/dcim/filtersets.py:1105 netbox/dcim/filtersets.py:1428 -#: netbox/dcim/filtersets.py:1526 netbox/dcim/filtersets.py:2191 -#: netbox/dcim/filtersets.py:2433 netbox/dcim/filtersets.py:2492 +#: netbox/dcim/filtersets.py:1526 netbox/dcim/filtersets.py:2206 +#: netbox/dcim/filtersets.py:2448 netbox/dcim/filtersets.py:2507 msgid "Site name (slug)" msgstr "" @@ -3925,44 +3926,44 @@ msgstr "" msgid "Virtual circuit termination (ID)" msgstr "" -#: netbox/dcim/filtersets.py:2056 +#: netbox/dcim/filtersets.py:2071 msgid "Parent module bay (ID)" msgstr "" -#: netbox/dcim/filtersets.py:2061 +#: netbox/dcim/filtersets.py:2076 msgid "Installed module (ID)" msgstr "" -#: netbox/dcim/filtersets.py:2072 +#: netbox/dcim/filtersets.py:2087 msgid "Installed device (ID)" msgstr "" -#: netbox/dcim/filtersets.py:2078 +#: netbox/dcim/filtersets.py:2093 msgid "Installed device (name)" msgstr "" -#: netbox/dcim/filtersets.py:2148 +#: netbox/dcim/filtersets.py:2163 msgid "Master (ID)" msgstr "" -#: netbox/dcim/filtersets.py:2154 +#: netbox/dcim/filtersets.py:2169 msgid "Master (name)" msgstr "" -#: netbox/dcim/filtersets.py:2196 netbox/tenancy/filtersets.py:250 +#: netbox/dcim/filtersets.py:2211 netbox/tenancy/filtersets.py:250 msgid "Tenant (ID)" msgstr "" -#: netbox/dcim/filtersets.py:2202 netbox/extras/filtersets.py:711 +#: netbox/dcim/filtersets.py:2217 netbox/extras/filtersets.py:711 #: netbox/tenancy/filtersets.py:256 msgid "Tenant (slug)" msgstr "" -#: netbox/dcim/filtersets.py:2238 netbox/dcim/forms/filtersets.py:1145 +#: netbox/dcim/filtersets.py:2253 netbox/dcim/forms/filtersets.py:1145 msgid "Unterminated" msgstr "" -#: netbox/dcim/filtersets.py:2496 +#: netbox/dcim/filtersets.py:2511 msgid "Power panel (ID)" msgstr "" From 95d0ca56a7292dc6db65b945b83eb4caec2f16b4 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Wed, 4 Jun 2025 11:48:23 -0500 Subject: [PATCH 28/30] Fixes #19487: fix ordering issues with CircuitTerminationTable/TunnelTerminationTable configuration (#19600) * Fixes #19487: make CircuitTermination.termination GFK not orderable * Add test to ensure no more broken sorting for CircuitTerminationTable * Fix CircuitTerminationTable.site_group accessor * Make TunnelTerminationTable.termination GFK field non-orderable --- netbox/circuits/tables/circuits.py | 5 +++-- netbox/circuits/tests/test_tables.py | 23 +++++++++++++++++++++++ netbox/vpn/tables/tunnels.py | 3 ++- 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 netbox/circuits/tests/test_tables.py diff --git a/netbox/circuits/tables/circuits.py b/netbox/circuits/tables/circuits.py index 3643446bd..901893a77 100644 --- a/netbox/circuits/tables/circuits.py +++ b/netbox/circuits/tables/circuits.py @@ -120,7 +120,8 @@ class CircuitTerminationTable(NetBoxTable): ) termination = tables.Column( verbose_name=_('Termination Point'), - linkify=True + linkify=True, + orderable=False, ) # Termination types @@ -132,7 +133,7 @@ class CircuitTerminationTable(NetBoxTable): site_group = tables.Column( verbose_name=_('Site Group'), linkify=True, - accessor='_sitegroup' + accessor='_site_group' ) region = tables.Column( verbose_name=_('Region'), diff --git a/netbox/circuits/tests/test_tables.py b/netbox/circuits/tests/test_tables.py new file mode 100644 index 000000000..2ab001c9b --- /dev/null +++ b/netbox/circuits/tests/test_tables.py @@ -0,0 +1,23 @@ +from django.test import RequestFactory, tag, TestCase + +from circuits.models import CircuitTermination +from circuits.tables import CircuitTerminationTable + + +@tag('regression') +class CircuitTerminationTableTest(TestCase): + def test_every_orderable_field_does_not_throw_exception(self): + terminations = CircuitTermination.objects.all() + disallowed = {'actions', } + + orderable_columns = [ + column.name for column in CircuitTerminationTable(terminations).columns + if column.orderable and column.name not in disallowed + ] + fake_request = RequestFactory().get("/") + + for col in orderable_columns: + for dir in ('-', ''): + table = CircuitTerminationTable(terminations) + table.order_by = f'{dir}{col}' + table.as_html(fake_request) diff --git a/netbox/vpn/tables/tunnels.py b/netbox/vpn/tables/tunnels.py index 94f65e573..d23317ac0 100644 --- a/netbox/vpn/tables/tunnels.py +++ b/netbox/vpn/tables/tunnels.py @@ -89,7 +89,8 @@ class TunnelTerminationTable(TenancyColumnsMixin, NetBoxTable): ) termination = tables.Column( verbose_name=_('Tunnel interface'), - linkify=True + linkify=True, + orderable=False, ) ip_addresses = columns.ManyToManyColumn( accessor=tables.A('termination__ip_addresses'), From d68f42140fc414a141d3b34371e6e5ec339a5da1 Mon Sep 17 00:00:00 2001 From: Martin Hauser Date: Wed, 4 Jun 2025 21:37:18 +0200 Subject: [PATCH 29/30] Closes #19535 - Add Project Stanza to pyproject toml (#19643) * feat(project): Add project metadata to pyproject.toml Introduces project metadata, including name, version, authors, and description, to `pyproject.toml` for enhanced package definition. Also includes URLs for source code, documentation, and issue tracking. * docs(release): Add checklist item for Python versions in pyproject.toml Include step to update minimum and supported Python versions in the project metadata file as part of the release process. * docs(release): Update checklist to include pyproject.toml versioning Add a step to update the version in `pyproject.toml` alongside `release.yaml`. * feat(project): Update pyproject.toml for best practices Refreshes metadata to resolve deprecations and follow packaging best practices. Updates include description, license, Python versions, classifiers, maintainers, and repository URLs for improved compliance. * fix(project): Update repository URL key in pyproject.toml Replaces the 'Repository' key with 'Source' in accordance with updated metadata conventions. This ensures compliance with modern best practices for project metadata. * fix(project): Specify Python 3 :: Only in classifiers Updates the Python version classifier in `pyproject.toml` to indicate support exclusively for Python 3. This change ensures clarity in the supported Python versions for the project metadata. --- docs/development/release-checklist.md | 3 ++- pyproject.toml | 34 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/docs/development/release-checklist.md b/docs/development/release-checklist.md index 342b2c3b3..75dd7f8a4 100644 --- a/docs/development/release-checklist.md +++ b/docs/development/release-checklist.md @@ -54,6 +54,7 @@ If a new Django release is adopted or other major dependencies (Python, PostgreS * Update the installation guide (`docs/installation/index.md`) with the new minimum versions. * Update the upgrade guide (`docs/installation/upgrading.md`) for the current version accordingly. * Update the minimum PostgreSQL version in the programming error template (`netbox/templates/exceptions/programming_error.html`). +* Update the minimum and supported Python versions in the project metadata file (`pyproject.toml`) ### Manually Perform a New Install @@ -165,7 +166,7 @@ Then, compile these portable (`.po`) files for use in the application: ### Update Version and Changelog -* Update the version number and date in `netbox/release.yaml`. Add or remove the designation (e.g. `beta1`) if applicable. +* Update the version number and date in `netbox/release.yaml` and `pyproject.toml`. Add or remove the designation (e.g. `beta1`) if applicable. * Update the example version numbers in the feature request and bug report templates under `.github/ISSUE_TEMPLATES/`. * Add a section for this release at the top of the changelog page for the minor version (e.g. `docs/release-notes/version-4.2.md`) listing all relevant changes made in this release. diff --git a/pyproject.toml b/pyproject.toml index 003465053..3664e4805 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,40 @@ # See PEP 518 for the spec of this file # https://www.python.org/dev/peps/pep-0518/ +[project] +name = "netbox" +version = "4.3.1" +requires-python = ">=3.10" +authors = [ + { name = "NetBox Community" } +] +maintainers = [ + { name = "NetBox Community" } +] +description = "The premier source of truth powering network automation." +readme = "README.md" +license = "Apache-2.0" +license-files = ["LICENSE.txt"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Framework :: Django", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Natural Language :: English", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] + + +[project.urls] +Homepage = "https://netboxlabs.com/products/netbox/" +Documentation = "https://netboxlabs.com/docs/netbox/" +Source = "https://github.com/netbox-community/netbox" +Issues = "https://github.com/netbox-community/netbox/issues" + [tool.black] line-length = 120 target_version = ['py310', 'py311', 'py312'] From e24fa2ee4d36d0a72346d055fc42fec7cae28820 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Wed, 4 Jun 2025 15:50:12 -0500 Subject: [PATCH 30/30] Fixes #19610: FieldError when sorting Tunnel Termination on tenant (#19612) --- netbox/vpn/tables/tunnels.py | 2 +- netbox/vpn/tests/test_tables.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 netbox/vpn/tests/test_tables.py diff --git a/netbox/vpn/tables/tunnels.py b/netbox/vpn/tables/tunnels.py index d23317ac0..fc8dec5e4 100644 --- a/netbox/vpn/tables/tunnels.py +++ b/netbox/vpn/tables/tunnels.py @@ -73,7 +73,7 @@ class TunnelTable(TenancyColumnsMixin, NetBoxTable): default_columns = ('pk', 'name', 'group', 'status', 'encapsulation', 'tenant', 'terminations_count') -class TunnelTerminationTable(TenancyColumnsMixin, NetBoxTable): +class TunnelTerminationTable(NetBoxTable): tunnel = tables.Column( verbose_name=_('Tunnel'), linkify=True diff --git a/netbox/vpn/tests/test_tables.py b/netbox/vpn/tests/test_tables.py new file mode 100644 index 000000000..0c7a4ae80 --- /dev/null +++ b/netbox/vpn/tests/test_tables.py @@ -0,0 +1,23 @@ +from django.test import RequestFactory, tag, TestCase + +from vpn.models import TunnelTermination +from vpn.tables import TunnelTerminationTable + + +@tag('regression') +class TunnelTerminationTableTest(TestCase): + def test_every_orderable_field_does_not_throw_exception(self): + terminations = TunnelTermination.objects.all() + fake_request = RequestFactory().get("/") + disallowed = {'actions'} + + orderable_columns = [ + column.name for column in TunnelTerminationTable(terminations).columns + if column.orderable and column.name not in disallowed + ] + + for col in orderable_columns: + for dir in ('-', ''): + table = TunnelTerminationTable(terminations) + table.order_by = f'{dir}{col}' + table.as_html(fake_request)