diff --git a/README.md b/README.md
index e3c9611c0..99ad9a597 100644
--- a/README.md
+++ b/README.md
@@ -58,8 +58,6 @@ as the cornerstone for network automation in thousands of organizations.
[](https://netboxlabs.com)
[](https://try.digitalocean.com/developer-cloud)
-
- [](https://ns1.com)
[](https://sentry.io)
diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md
index d682c9fca..5f086bca4 100644
--- a/docs/release-notes/version-3.4.md
+++ b/docs/release-notes/version-3.4.md
@@ -4,6 +4,7 @@
### Enhancements
+* [#12007](https://github.com/netbox-community/netbox/issues/12007) - Enable filtering of VM Interfaces by assigned VLAN
* [#12095](https://github.com/netbox-community/netbox/issues/12095) - Specify UTF-8 encoding for default export template MIME type
### Bug Fixes
@@ -14,6 +15,9 @@
* [#12084](https://github.com/netbox-community/netbox/issues/12084) - Fix exception when attempting to create a saved filter for applied filters
* [#12087](https://github.com/netbox-community/netbox/issues/12087) - Fix bulk editing of many-to-many relationships
* [#12117](https://github.com/netbox-community/netbox/issues/12117) - Hide clone button for objects with no clonable attributes
+* [#12118](https://github.com/netbox-community/netbox/issues/12118) - Fix instantiation of nested inventory item templates when creating a device
+* [#12184](https://github.com/netbox-community/netbox/issues/12184) - Fix filtered bulk deletion for various models
+* [#12190](https://github.com/netbox-community/netbox/issues/12190) - Fix form layout for plugin textarea fields
---
diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py
index 3168509ba..461113147 100644
--- a/netbox/circuits/views.py
+++ b/netbox/circuits/views.py
@@ -196,6 +196,7 @@ class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
queryset = CircuitType.objects.annotate(
circuit_count=count_related(Circuit, 'type')
)
+ filterset = filtersets.CircuitTypeFilterSet
table = tables.CircuitTypeTable
diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py
index 493ccbbea..fb59659d3 100644
--- a/netbox/dcim/filtersets.py
+++ b/netbox/dcim/filtersets.py
@@ -24,6 +24,7 @@ __all__ = (
'CableFilterSet',
'CabledObjectFilterSet',
'CableTerminationFilterSet',
+ 'CommonInterfaceFilterSet',
'ConsoleConnectionFilterSet',
'ConsolePortFilterSet',
'ConsolePortTemplateFilterSet',
@@ -1321,11 +1322,45 @@ class PowerOutletFilterSet(
fields = ['id', 'name', 'label', 'feed_leg', 'description', 'cable_end']
+class CommonInterfaceFilterSet(django_filters.FilterSet):
+ vlan_id = django_filters.CharFilter(
+ method='filter_vlan_id',
+ label=_('Assigned VLAN')
+ )
+ vlan = django_filters.CharFilter(
+ method='filter_vlan',
+ label=_('Assigned VID')
+ )
+ vrf_id = django_filters.ModelMultipleChoiceFilter(
+ field_name='vrf',
+ queryset=VRF.objects.all(),
+ label=_('VRF'),
+ )
+ vrf = django_filters.ModelMultipleChoiceFilter(
+ field_name='vrf__rd',
+ queryset=VRF.objects.all(),
+ to_field_name='rd',
+ label=_('VRF (RD)'),
+ )
+ l2vpn_id = django_filters.ModelMultipleChoiceFilter(
+ field_name='l2vpn_terminations__l2vpn',
+ queryset=L2VPN.objects.all(),
+ label=_('L2VPN (ID)'),
+ )
+ l2vpn = django_filters.ModelMultipleChoiceFilter(
+ field_name='l2vpn_terminations__l2vpn__identifier',
+ queryset=L2VPN.objects.all(),
+ to_field_name='identifier',
+ label=_('L2VPN'),
+ )
+
+
class InterfaceFilterSet(
ModularDeviceComponentFilterSet,
NetBoxModelFilterSet,
CabledObjectFilterSet,
- PathEndpointFilterSet
+ PathEndpointFilterSet,
+ CommonInterfaceFilterSet
):
# Override device and device_id filters from DeviceComponentFilterSet to match against any peer virtual chassis
# members
@@ -1370,14 +1405,6 @@ class InterfaceFilterSet(
poe_type = django_filters.MultipleChoiceFilter(
choices=InterfacePoETypeChoices
)
- vlan_id = django_filters.CharFilter(
- method='filter_vlan_id',
- label=_('Assigned VLAN')
- )
- vlan = django_filters.CharFilter(
- method='filter_vlan',
- label=_('Assigned VID')
- )
type = django_filters.MultipleChoiceFilter(
choices=InterfaceTypeChoices,
null_value=None
@@ -1388,17 +1415,6 @@ class InterfaceFilterSet(
rf_channel = django_filters.MultipleChoiceFilter(
choices=WirelessChannelChoices
)
- vrf_id = django_filters.ModelMultipleChoiceFilter(
- field_name='vrf',
- queryset=VRF.objects.all(),
- label=_('VRF'),
- )
- vrf = django_filters.ModelMultipleChoiceFilter(
- field_name='vrf__rd',
- queryset=VRF.objects.all(),
- to_field_name='rd',
- label=_('VRF (RD)'),
- )
vdc_id = django_filters.ModelMultipleChoiceFilter(
field_name='vdcs',
queryset=VirtualDeviceContext.objects.all(),
@@ -1416,17 +1432,6 @@ class InterfaceFilterSet(
to_field_name='name',
label='Virtual Device Context',
)
- l2vpn_id = django_filters.ModelMultipleChoiceFilter(
- field_name='l2vpn_terminations__l2vpn',
- queryset=L2VPN.objects.all(),
- label=_('L2VPN (ID)'),
- )
- l2vpn = django_filters.ModelMultipleChoiceFilter(
- field_name='l2vpn_terminations__l2vpn__identifier',
- queryset=L2VPN.objects.all(),
- to_field_name='identifier',
- label=_('L2VPN'),
- )
class Meta:
model = Interface
diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py
index 1429003c5..0526c49cb 100644
--- a/netbox/dcim/models/devices.py
+++ b/netbox/dcim/models/devices.py
@@ -774,8 +774,10 @@ class Device(PrimaryModel, ConfigContextModel):
bulk_create: If True, bulk_create() will be called to create all components in a single query
(default). Otherwise, save() will be called on each instance individually.
"""
- components = [obj.instantiate(device=self) for obj in queryset]
- if components and bulk_create:
+ if bulk_create:
+ components = [obj.instantiate(device=self) for obj in queryset]
+ if not components:
+ return
model = components[0]._meta.model
model.objects.bulk_create(components)
# Manually send the post_save signal for each of the newly created components
@@ -788,8 +790,9 @@ class Device(PrimaryModel, ConfigContextModel):
using='default',
update_fields=None
)
- elif components:
- for component in components:
+ else:
+ for obj in queryset:
+ component = obj.instantiate(device=self)
component.save()
def save(self, *args, **kwargs):
diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py
index 9b49e799c..5a6261eba 100644
--- a/netbox/dcim/views.py
+++ b/netbox/dcim/views.py
@@ -628,6 +628,7 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView):
queryset = RackRole.objects.annotate(
rack_count=count_related(Rack, 'role')
)
+ filterset = filtersets.RackRoleFilterSet
table = tables.RackRoleTable
@@ -909,6 +910,7 @@ class ManufacturerBulkDeleteView(generic.BulkDeleteView):
queryset = Manufacturer.objects.annotate(
devicetype_count=count_related(DeviceType, 'manufacturer')
)
+ filterset = filtersets.ManufacturerFilterSet
table = tables.ManufacturerTable
@@ -1808,6 +1810,7 @@ class DeviceRoleBulkDeleteView(generic.BulkDeleteView):
device_count=count_related(Device, 'device_role'),
vm_count=count_related(VirtualMachine, 'role')
)
+ filterset = filtersets.DeviceRoleFilterSet
table = tables.DeviceRoleTable
@@ -1868,6 +1871,7 @@ class PlatformBulkEditView(generic.BulkEditView):
class PlatformBulkDeleteView(generic.BulkDeleteView):
queryset = Platform.objects.all()
+ filterset = filtersets.PlatformFilterSet
table = tables.PlatformTable
@@ -2981,6 +2985,7 @@ class InventoryItemBulkRenameView(generic.BulkRenameView):
class InventoryItemBulkDeleteView(generic.BulkDeleteView):
queryset = InventoryItem.objects.all()
+ filterset = filtersets.InventoryItemFilterSet
table = tables.InventoryItemTable
template_name = 'dcim/inventoryitem_bulk_delete.html'
@@ -3038,6 +3043,7 @@ class InventoryItemRoleBulkDeleteView(generic.BulkDeleteView):
queryset = InventoryItemRole.objects.annotate(
inventoryitem_count=count_related(InventoryItem, 'role'),
)
+ filterset = filtersets.InventoryItemRoleFilterSet
table = tables.InventoryItemRoleTable
diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py
index 81a607eec..4b5efda3c 100644
--- a/netbox/extras/tests/test_api.py
+++ b/netbox/extras/tests/test_api.py
@@ -1,13 +1,10 @@
import datetime
-from unittest import skipIf
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from django.utils.timezone import make_aware
-from django_rq.queues import get_connection
from rest_framework import status
-from rq import Worker
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site
from extras.api.views import ReportViewSet, ScriptViewSet
@@ -16,8 +13,6 @@ from extras.reports import Report
from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
from utilities.testing import APITestCase, APIViewTestCases
-rq_worker_running = Worker.count(get_connection('default'))
-
class AppTest(APITestCase):
@@ -539,16 +534,6 @@ class ReportTest(APITestCase):
self.assertEqual(response.data['name'], self.TestReport.__name__)
- @skipIf(not rq_worker_running, "RQ worker not running")
- def test_run_report(self):
- self.add_permissions('extras.run_script')
-
- url = reverse('extras-api:report-run', kwargs={'pk': None})
- response = self.client.post(url, {}, format='json', **self.header)
- self.assertHttpStatus(response, status.HTTP_200_OK)
-
- self.assertEqual(response.data['result']['status']['value'], 'pending')
-
class ScriptTest(APITestCase):
@@ -589,27 +574,6 @@ class ScriptTest(APITestCase):
self.assertEqual(response.data['vars']['var2'], 'IntegerVar')
self.assertEqual(response.data['vars']['var3'], 'BooleanVar')
- @skipIf(not rq_worker_running, "RQ worker not running")
- def test_run_script(self):
- self.add_permissions('extras.run_script')
-
- script_data = {
- 'var1': 'FooBar',
- 'var2': 123,
- 'var3': False,
- }
-
- data = {
- 'data': script_data,
- 'commit': True,
- }
-
- url = reverse('extras-api:script-detail', kwargs={'pk': None})
- response = self.client.post(url, data, format='json', **self.header)
- self.assertHttpStatus(response, status.HTTP_200_OK)
-
- self.assertEqual(response.data['result']['status']['value'], 'pending')
-
class CreatedUpdatedFilterTest(APITestCase):
diff --git a/netbox/extras/views.py b/netbox/extras/views.py
index 91d3b5c58..d8a3c377d 100644
--- a/netbox/extras/views.py
+++ b/netbox/extras/views.py
@@ -414,6 +414,7 @@ class ConfigContextDeleteView(generic.ObjectDeleteView):
class ConfigContextBulkDeleteView(generic.BulkDeleteView):
queryset = ConfigContext.objects.all()
+ filterset = filtersets.ConfigContextFilterSet
table = tables.ConfigContextTable
diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py
index ea6441650..5af1bc769 100644
--- a/netbox/ipam/tests/test_api.py
+++ b/netbox/ipam/tests/test_api.py
@@ -836,7 +836,7 @@ class VLANTest(APIViewTestCases.APIViewTestCase):
self.add_permissions('ipam.delete_vlan')
url = reverse('ipam-api:vlan-detail', kwargs={'pk': vlan.pk})
- with disable_warnings('django.request'):
+ with disable_warnings('netbox.api.views.ModelViewSet'):
response = self.client.delete(url, **self.header)
self.assertHttpStatus(response, status.HTTP_409_CONFLICT)
diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py
index 130014f3f..5f10fb5a7 100644
--- a/netbox/ipam/views.py
+++ b/netbox/ipam/views.py
@@ -431,6 +431,7 @@ class RoleBulkEditView(generic.BulkEditView):
class RoleBulkDeleteView(generic.BulkDeleteView):
queryset = Role.objects.all()
+ filterset = filtersets.RoleFilterSet
table = tables.RoleTable
diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py
index c0efe3014..211b869e6 100644
--- a/netbox/netbox/views/generic/bulk_views.py
+++ b/netbox/netbox/views/generic/bulk_views.py
@@ -16,7 +16,6 @@ from django_tables2.export import TableExport
from extras.models import ExportTemplate
from extras.signals import clear_webhooks
-from utilities.choices import ImportFormatChoices
from utilities.error_handlers import handle_protectederror
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
from utilities.forms import BulkRenameForm, ConfirmationForm, ImportForm, restrict_form_fields
diff --git a/netbox/templates/dcim/cable_edit.html b/netbox/templates/dcim/cable_edit.html
index 1c747b44b..0ad0637e3 100644
--- a/netbox/templates/dcim/cable_edit.html
+++ b/netbox/templates/dcim/cable_edit.html
@@ -98,7 +98,7 @@