Просмотр исходного кода

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
bctiemann 1 год назад
Родитель
Сommit
f873735dd4
3 измененных файлов с 68 добавлено и 23 удалено
  1. 45 17
      netbox/dcim/forms/bulk_edit.py
  2. 11 1
      netbox/dcim/views.py
  3. 12 5
      netbox/netbox/views/generic/bulk_views.py

+ 45 - 17
netbox/dcim/forms/bulk_edit.py

@@ -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:
-            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:
+        if not self.device_id:
             # 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 = ()

+ 11 - 1
netbox/dcim/views.py

@@ -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()

+ 12 - 5
netbox/netbox/views/generic/bulk_views.py

@@ -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
-            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'])
+            self.post_save_operations(form, obj)
 
 
         # Rebuild the tree for MPTT models
         # Rebuild the tree for MPTT models
         if issubclass(self.queryset.model, MPTTModel):
         if issubclass(self.queryset.model, MPTTModel):