Skip to content

Commit f873735

Browse files
authored
Closes: #15239 - Allow adding/removing tagged VLANs in bulk editing of Interfaces (#17524)
* Allow adding/removing tagged VLANs in bulk editing of Interfaces * Move vlan/interface-specific field operations to an overrideable method * Ensure interfaces are MODE_TAGGED before adding/removing tagged vlans * Add docstring for generic extra_object_field_operations * Move tagging ops into post_save_operations and use a TabbedGroup in the form
1 parent 6035ad1 commit f873735

File tree

3 files changed

+68
-23
lines changed

3 files changed

+68
-23
lines changed

netbox/dcim/forms/bulk_edit.py

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from users.models import User
1414
from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
1515
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
16-
from utilities.forms.rendering import FieldSet, InlineFields
16+
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
1717
from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions
1818
from wireless.models import WirelessLAN, WirelessLANGroup
1919
from wireless.choices import WirelessRoleChoices
@@ -1404,18 +1404,25 @@ class InterfaceBulkEditForm(
14041404
parent = DynamicModelChoiceField(
14051405
label=_('Parent'),
14061406
queryset=Interface.objects.all(),
1407-
required=False
1407+
required=False,
1408+
query_params={
1409+
'virtual_chassis_member_id': '$device',
1410+
}
14081411
)
14091412
bridge = DynamicModelChoiceField(
14101413
label=_('Bridge'),
14111414
queryset=Interface.objects.all(),
1412-
required=False
1415+
required=False,
1416+
query_params={
1417+
'virtual_chassis_member_id': '$device',
1418+
}
14131419
)
14141420
lag = DynamicModelChoiceField(
14151421
queryset=Interface.objects.all(),
14161422
required=False,
14171423
query_params={
14181424
'type': 'lag',
1425+
'virtual_chassis_member_id': '$device',
14191426
},
14201427
label=_('LAG')
14211428
)
@@ -1472,6 +1479,7 @@ class InterfaceBulkEditForm(
14721479
required=False,
14731480
query_params={
14741481
'group_id': '$vlan_group',
1482+
'available_on_device': '$device',
14751483
},
14761484
label=_('Untagged VLAN')
14771485
)
@@ -1480,9 +1488,28 @@ class InterfaceBulkEditForm(
14801488
required=False,
14811489
query_params={
14821490
'group_id': '$vlan_group',
1491+
'available_on_device': '$device',
14831492
},
14841493
label=_('Tagged VLANs')
14851494
)
1495+
add_tagged_vlans = DynamicModelMultipleChoiceField(
1496+
label=_('Add tagged VLANs'),
1497+
queryset=VLAN.objects.all(),
1498+
required=False,
1499+
query_params={
1500+
'group_id': '$vlan_group',
1501+
'available_on_device': '$device',
1502+
},
1503+
)
1504+
remove_tagged_vlans = DynamicModelMultipleChoiceField(
1505+
label=_('Remove tagged VLANs'),
1506+
queryset=VLAN.objects.all(),
1507+
required=False,
1508+
query_params={
1509+
'group_id': '$vlan_group',
1510+
'available_on_device': '$device',
1511+
}
1512+
)
14861513
vrf = DynamicModelChoiceField(
14871514
queryset=VRF.objects.all(),
14881515
required=False,
@@ -1509,7 +1536,13 @@ class InterfaceBulkEditForm(
15091536
FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')),
15101537
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
15111538
FieldSet('parent', 'bridge', 'lag', name=_('Related Interfaces')),
1512-
FieldSet('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', name=_('802.1Q Switching')),
1539+
FieldSet('mode', 'vlan_group', 'untagged_vlan', name=_('802.1Q Switching')),
1540+
FieldSet(
1541+
TabbedGroups(
1542+
FieldSet('tagged_vlans', name=_('Assignment')),
1543+
FieldSet('add_tagged_vlans', 'remove_tagged_vlans', name=_('Add/Remove')),
1544+
),
1545+
),
15131546
FieldSet(
15141547
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
15151548
name=_('Wireless')
@@ -1523,19 +1556,7 @@ class InterfaceBulkEditForm(
15231556

15241557
def __init__(self, *args, **kwargs):
15251558
super().__init__(*args, **kwargs)
1526-
if self.device_id:
1527-
device = Device.objects.filter(pk=self.device_id).first()
1528-
1529-
# Restrict parent/bridge/LAG interface assignment by device
1530-
self.fields['parent'].widget.add_query_param('virtual_chassis_member_id', device.pk)
1531-
self.fields['bridge'].widget.add_query_param('virtual_chassis_member_id', device.pk)
1532-
self.fields['lag'].widget.add_query_param('virtual_chassis_member_id', device.pk)
1533-
1534-
# Limit VLAN choices by device
1535-
self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device.pk)
1536-
self.fields['tagged_vlans'].widget.add_query_param('available_on_device', device.pk)
1537-
1538-
else:
1559+
if not self.device_id:
15391560
# See #4523
15401561
if 'pk' in self.initial:
15411562
site = None
@@ -1559,6 +1580,13 @@ def __init__(self, *args, **kwargs):
15591580
'site_id', [site.pk, settings.FILTERS_NULL_CHOICE_VALUE]
15601581
)
15611582

1583+
self.fields['add_tagged_vlans'].widget.add_query_param(
1584+
'site_id', [site.pk, settings.FILTERS_NULL_CHOICE_VALUE]
1585+
)
1586+
self.fields['remove_tagged_vlans'].widget.add_query_param(
1587+
'site_id', [site.pk, settings.FILTERS_NULL_CHOICE_VALUE]
1588+
)
1589+
15621590
self.fields['parent'].choices = ()
15631591
self.fields['parent'].widget.attrs['disabled'] = True
15641592
self.fields['bridge'].choices = ()

netbox/dcim/views.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from virtualization.models import VirtualMachine
3636
from virtualization.tables import VirtualMachineTable
3737
from . import filtersets, forms, tables
38-
from .choices import DeviceFaceChoices
38+
from .choices import DeviceFaceChoices, InterfaceModeChoices
3939
from .models import *
4040

4141
CABLE_TERMINATION_TYPES = {
@@ -2616,6 +2616,16 @@ class InterfaceBulkEditView(generic.BulkEditView):
26162616
table = tables.InterfaceTable
26172617
form = forms.InterfaceBulkEditForm
26182618

2619+
def post_save_operations(self, form, obj):
2620+
super().post_save_operations(form, obj)
2621+
2622+
# Add/remove tagged VLANs
2623+
if obj.mode == InterfaceModeChoices.MODE_TAGGED:
2624+
if form.cleaned_data.get('add_tagged_vlans', None):
2625+
obj.tagged_vlans.add(*form.cleaned_data['add_tagged_vlans'])
2626+
if form.cleaned_data.get('remove_tagged_vlans', None):
2627+
obj.tagged_vlans.remove(*form.cleaned_data['remove_tagged_vlans'])
2628+
26192629

26202630
class InterfaceBulkRenameView(generic.BulkRenameView):
26212631
queryset = Interface.objects.all()

netbox/netbox/views/generic/bulk_views.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,17 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
541541
def get_required_permission(self):
542542
return get_permission_for_model(self.queryset.model, 'change')
543543

544+
def post_save_operations(self, form, obj):
545+
"""
546+
This method is called for each object in _update_objects. Override to perform additional object-level
547+
operations that are specific to a particular ModelForm.
548+
"""
549+
# Add/remove tags
550+
if form.cleaned_data.get('add_tags', None):
551+
obj.tags.add(*form.cleaned_data['add_tags'])
552+
if form.cleaned_data.get('remove_tags', None):
553+
obj.tags.remove(*form.cleaned_data['remove_tags'])
554+
544555
def _update_objects(self, form, request):
545556
custom_fields = getattr(form, 'custom_fields', {})
546557
standard_fields = [
@@ -612,11 +623,7 @@ def _update_objects(self, form, request):
612623
elif form.cleaned_data[name]:
613624
getattr(obj, name).set(form.cleaned_data[name])
614625

615-
# Add/remove tags
616-
if form.cleaned_data.get('add_tags', None):
617-
obj.tags.add(*form.cleaned_data['add_tags'])
618-
if form.cleaned_data.get('remove_tags', None):
619-
obj.tags.remove(*form.cleaned_data['remove_tags'])
626+
self.post_save_operations(form, obj)
620627

621628
# Rebuild the tree for MPTT models
622629
if issubclass(self.queryset.model, MPTTModel):

0 commit comments

Comments
 (0)