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
This commit is contained in:
bctiemann 2024-11-07 09:14:33 -05:00 committed by GitHub
parent 6035ad139a
commit f873735dd4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 68 additions and 23 deletions

View File

@ -13,7 +13,7 @@ from tenancy.models import Tenant
from users.models import User from users.models import User
from utilities.forms import BulkEditForm, add_blank_choice, form_from_model from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.rendering import FieldSet, InlineFields from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions
from wireless.models import WirelessLAN, WirelessLANGroup from wireless.models import WirelessLAN, WirelessLANGroup
from wireless.choices import WirelessRoleChoices from wireless.choices import WirelessRoleChoices
@ -1404,18 +1404,25 @@ class InterfaceBulkEditForm(
parent = DynamicModelChoiceField( parent = DynamicModelChoiceField(
label=_('Parent'), label=_('Parent'),
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
required=False required=False,
query_params={
'virtual_chassis_member_id': '$device',
}
) )
bridge = DynamicModelChoiceField( bridge = DynamicModelChoiceField(
label=_('Bridge'), label=_('Bridge'),
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
required=False required=False,
query_params={
'virtual_chassis_member_id': '$device',
}
) )
lag = DynamicModelChoiceField( lag = DynamicModelChoiceField(
queryset=Interface.objects.all(), queryset=Interface.objects.all(),
required=False, required=False,
query_params={ query_params={
'type': 'lag', 'type': 'lag',
'virtual_chassis_member_id': '$device',
}, },
label=_('LAG') label=_('LAG')
) )
@ -1472,6 +1479,7 @@ class InterfaceBulkEditForm(
required=False, required=False,
query_params={ query_params={
'group_id': '$vlan_group', 'group_id': '$vlan_group',
'available_on_device': '$device',
}, },
label=_('Untagged VLAN') label=_('Untagged VLAN')
) )
@ -1480,9 +1488,28 @@ class InterfaceBulkEditForm(
required=False, required=False,
query_params={ query_params={
'group_id': '$vlan_group', 'group_id': '$vlan_group',
'available_on_device': '$device',
}, },
label=_('Tagged VLANs') label=_('Tagged VLANs')
) )
add_tagged_vlans = DynamicModelMultipleChoiceField(
label=_('Add tagged VLANs'),
queryset=VLAN.objects.all(),
required=False,
query_params={
'group_id': '$vlan_group',
'available_on_device': '$device',
},
)
remove_tagged_vlans = DynamicModelMultipleChoiceField(
label=_('Remove tagged VLANs'),
queryset=VLAN.objects.all(),
required=False,
query_params={
'group_id': '$vlan_group',
'available_on_device': '$device',
}
)
vrf = DynamicModelChoiceField( vrf = DynamicModelChoiceField(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
required=False, required=False,
@ -1509,7 +1536,13 @@ class InterfaceBulkEditForm(
FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')), FieldSet('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected', name=_('Operation')),
FieldSet('poe_mode', 'poe_type', name=_('PoE')), FieldSet('poe_mode', 'poe_type', name=_('PoE')),
FieldSet('parent', 'bridge', 'lag', name=_('Related Interfaces')), FieldSet('parent', 'bridge', 'lag', name=_('Related Interfaces')),
FieldSet('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', name=_('802.1Q Switching')), FieldSet('mode', 'vlan_group', 'untagged_vlan', name=_('802.1Q Switching')),
FieldSet(
TabbedGroups(
FieldSet('tagged_vlans', name=_('Assignment')),
FieldSet('add_tagged_vlans', 'remove_tagged_vlans', name=_('Add/Remove')),
),
),
FieldSet( FieldSet(
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
name=_('Wireless') name=_('Wireless')
@ -1523,19 +1556,7 @@ class InterfaceBulkEditForm(
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if self.device_id: if not self.device_id:
device = Device.objects.filter(pk=self.device_id).first()
# Restrict parent/bridge/LAG interface assignment by device
self.fields['parent'].widget.add_query_param('virtual_chassis_member_id', device.pk)
self.fields['bridge'].widget.add_query_param('virtual_chassis_member_id', device.pk)
self.fields['lag'].widget.add_query_param('virtual_chassis_member_id', device.pk)
# Limit VLAN choices by device
self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device.pk)
self.fields['tagged_vlans'].widget.add_query_param('available_on_device', device.pk)
else:
# See #4523 # See #4523
if 'pk' in self.initial: if 'pk' in self.initial:
site = None site = None
@ -1559,6 +1580,13 @@ class InterfaceBulkEditForm(
'site_id', [site.pk, settings.FILTERS_NULL_CHOICE_VALUE] 'site_id', [site.pk, settings.FILTERS_NULL_CHOICE_VALUE]
) )
self.fields['add_tagged_vlans'].widget.add_query_param(
'site_id', [site.pk, settings.FILTERS_NULL_CHOICE_VALUE]
)
self.fields['remove_tagged_vlans'].widget.add_query_param(
'site_id', [site.pk, settings.FILTERS_NULL_CHOICE_VALUE]
)
self.fields['parent'].choices = () self.fields['parent'].choices = ()
self.fields['parent'].widget.attrs['disabled'] = True self.fields['parent'].widget.attrs['disabled'] = True
self.fields['bridge'].choices = () self.fields['bridge'].choices = ()

View File

@ -35,7 +35,7 @@ from virtualization.forms import VirtualMachineFilterForm
from virtualization.models import VirtualMachine from virtualization.models import VirtualMachine
from virtualization.tables import VirtualMachineTable from virtualization.tables import VirtualMachineTable
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .choices import DeviceFaceChoices from .choices import DeviceFaceChoices, InterfaceModeChoices
from .models import * from .models import *
CABLE_TERMINATION_TYPES = { CABLE_TERMINATION_TYPES = {
@ -2616,6 +2616,16 @@ class InterfaceBulkEditView(generic.BulkEditView):
table = tables.InterfaceTable table = tables.InterfaceTable
form = forms.InterfaceBulkEditForm form = forms.InterfaceBulkEditForm
def post_save_operations(self, form, obj):
super().post_save_operations(form, obj)
# Add/remove tagged VLANs
if obj.mode == InterfaceModeChoices.MODE_TAGGED:
if form.cleaned_data.get('add_tagged_vlans', None):
obj.tagged_vlans.add(*form.cleaned_data['add_tagged_vlans'])
if form.cleaned_data.get('remove_tagged_vlans', None):
obj.tagged_vlans.remove(*form.cleaned_data['remove_tagged_vlans'])
class InterfaceBulkRenameView(generic.BulkRenameView): class InterfaceBulkRenameView(generic.BulkRenameView):
queryset = Interface.objects.all() queryset = Interface.objects.all()

View File

@ -541,6 +541,17 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
def get_required_permission(self): def get_required_permission(self):
return get_permission_for_model(self.queryset.model, 'change') return get_permission_for_model(self.queryset.model, 'change')
def post_save_operations(self, form, obj):
"""
This method is called for each object in _update_objects. Override to perform additional object-level
operations that are specific to a particular ModelForm.
"""
# Add/remove tags
if form.cleaned_data.get('add_tags', None):
obj.tags.add(*form.cleaned_data['add_tags'])
if form.cleaned_data.get('remove_tags', None):
obj.tags.remove(*form.cleaned_data['remove_tags'])
def _update_objects(self, form, request): def _update_objects(self, form, request):
custom_fields = getattr(form, 'custom_fields', {}) custom_fields = getattr(form, 'custom_fields', {})
standard_fields = [ standard_fields = [
@ -612,11 +623,7 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
elif form.cleaned_data[name]: elif form.cleaned_data[name]:
getattr(obj, name).set(form.cleaned_data[name]) getattr(obj, name).set(form.cleaned_data[name])
# Add/remove tags self.post_save_operations(form, obj)
if form.cleaned_data.get('add_tags', None):
obj.tags.add(*form.cleaned_data['add_tags'])
if form.cleaned_data.get('remove_tags', None):
obj.tags.remove(*form.cleaned_data['remove_tags'])
# Rebuild the tree for MPTT models # Rebuild the tree for MPTT models
if issubclass(self.queryset.model, MPTTModel): if issubclass(self.queryset.model, MPTTModel):