Repository: netbox-community/netbox

Issue:
Expand Edit Interface 802.1Q VLAN form search query
<!--
    NOTE: This form is only for proposing specific new features or enhancements.
    If you have a general idea or question, please post to our mailing list
    instead of opening an issue:

        https://groups.google.com/forum/#!forum/netbox-discuss

    NOTE: Due to an excessive backlog of feature requests, we are not currently
    accepting any proposals which significantly extend NetBox's feature scope.

    Please describe the environment in which you are running NetBox. Be sure
    that you are running an unmodified instance of the latest stable release
    before submitting a bug report.
-->
### Environment
* Python version:  3.6.8<!-- Example: 3.5.4 -->
* NetBox version:  2.6.7<!-- Example: 2.3.6 -->

<!--
    Describe in detail the new functionality you are proposing. Include any
    specific changes to work flows, data models, or the user interface.
-->
### Proposed Functionality
When using the GUI Select2 fields for adding untagged/tagged VLANs to a device/VM interface, expand the search parameters to also search by site and VLAN group. Currently it appears that only the VLAN Name and VID are used. The dropdown currently displays the sites in the dropdown list, but they're not part of the search parameters.
<!--
    Convey an example use case for your proposed feature. Write from the
    perspective of a NetBox user who would benefit from the proposed
    functionality and describe how.
--->
### Use Case
When you have a cookiecutter design for a particular type of site, the VIDs and VLAN names are repeated identically, and added to their respective sites and VLAN groups, if applicable.

When you go to add those VLANs to an interface and search for vlan `5` for example, you may get dozens or hundreds of results because that VID and name are repeated lots of times. This makes it nearly impossible to add the VLAN through the GUI. 

A potential workaround with the current state is to make all the VLAN names unique by adding the site name to the VLAN name, but this seems repetitive and less flexible, should the site name ever change for example.

A cursory glance makes this appear to be an issue with the REST API (and the `q` param) but there is also a `site` filter param that could be used in this case.
<!--
    Note any changes to the database schema necessary to support the new
    feature. For example, does the proposal require adding a new model or
    field? (Not all new features require database changes.)
--->
### Database Changes
N/A

<!--
    List any new dependencies on external libraries or services that this new
    feature would introduce. For example, does the proposal require the
    installation of a new Python package? (Not all new features introduce new
    dependencies.)
-->
### External Dependencies
N/A


Discussion:

Resolving patch:
diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py
--- a/netbox/dcim/forms.py
+++ b/netbox/dcim/forms.py
@@ -2826,7 +2826,10 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
         widget=APISelect(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
     )
     tagged_vlans = forms.ModelMultipleChoiceField(
@@ -2835,7 +2838,10 @@ class InterfaceForm(InterfaceCommonForm, BootstrapMixin, forms.ModelForm):
         widget=APISelectMultiple(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
     )
 
@@ -2865,18 +2871,20 @@ class Meta:
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
 
-        # Limit LAG choices to interfaces belonging to this device (or VC master)
         if self.is_bound:
             device = Device.objects.get(pk=self.data['device'])
-            self.fields['lag'].queryset = Interface.objects.filter(
-                device__in=[device, device.get_vc_master()],
-                type=InterfaceTypeChoices.TYPE_LAG
-            )
         else:
-            self.fields['lag'].queryset = Interface.objects.filter(
-                device__in=[self.instance.device, self.instance.device.get_vc_master()],
-                type=InterfaceTypeChoices.TYPE_LAG
-            )
+            device = self.instance.device
+
+        # Limit LAG choices to interfaces belonging to this device (or VC master)
+        self.fields['lag'].queryset = Interface.objects.filter(
+            device__in=[device, device.get_vc_master()],
+            type=InterfaceTypeChoices.TYPE_LAG
+        )
+
+        # Add current site to VLANs query params
+        self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', device.site.pk)
+        self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', device.site.pk)
 
 
 class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form):
@@ -2936,7 +2944,10 @@ class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form):
         widget=APISelect(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
     )
     tagged_vlans = forms.ModelMultipleChoiceField(
@@ -2945,7 +2956,10 @@ class InterfaceCreateForm(BootstrapMixin, InterfaceCommonForm, forms.Form):
         widget=APISelectMultiple(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
     )
 
@@ -2961,6 +2975,10 @@ def __init__(self, *args, **kwargs):
             type=InterfaceTypeChoices.TYPE_LAG
         )
 
+        # Add current site to VLANs query params
+        self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', device.site.pk)
+        self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', device.site.pk)
+
 
 class InterfaceCSVForm(forms.ModelForm):
     device = FlexibleModelChoiceField(
@@ -3084,7 +3102,10 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
         widget=APISelect(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
     )
     tagged_vlans = forms.ModelMultipleChoiceField(
@@ -3093,7 +3114,10 @@ class InterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
         widget=APISelectMultiple(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
     )
 
@@ -3112,6 +3136,10 @@ def __init__(self, *args, **kwargs):
                 device__in=[device, device.get_vc_master()],
                 type=InterfaceTypeChoices.TYPE_LAG
             )
+
+            # Add current site to VLANs query params
+            self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', device.site.pk)
+            self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', device.site.pk)
         else:
             self.fields['lag'].choices = ()
             self.fields['lag'].widget.attrs['disabled'] = True
diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py
--- a/netbox/utilities/forms.py
+++ b/netbox/utilities/forms.py
@@ -312,12 +312,17 @@ def add_filter_for(self, name, value):
 
     def add_additional_query_param(self, name, value):
         """
-        Add details for an additional query param in the form of a data-* attribute.
+        Add details for an additional query param in the form of a data-* JSON-encoded list attribute.
 
         :param name: The name of the query param
         :param value: The value of the query param
         """
-        self.attrs['data-additional-query-param-{}'.format(name)] = value
+        key = 'data-additional-query-param-{}'.format(name)
+
+        values = json.loads(self.attrs.get(key, '[]'))
+        values.append(value)
+
+        self.attrs[key] = json.dumps(values)
 
     def add_conditional_query_param(self, condition, value):
         """
diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py
--- a/netbox/virtualization/forms.py
+++ b/netbox/virtualization/forms.py
@@ -654,7 +654,10 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
         widget=APISelect(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
     )
     tagged_vlans = forms.ModelMultipleChoiceField(
@@ -663,7 +666,10 @@ class InterfaceForm(BootstrapMixin, forms.ModelForm):
         widget=APISelectMultiple(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
     )
     tags = TagField(
@@ -691,35 +697,12 @@ class Meta:
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
 
-        # Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site
-        vlan_choices = []
-        global_vlans = VLAN.objects.filter(site=None, group=None)
-        vlan_choices.append(
-            ('Global', [(vlan.pk, vlan) for vlan in global_vlans])
-        )
-        for group in VLANGroup.objects.filter(site=None):
-            global_group_vlans = VLAN.objects.filter(group=group)
-            vlan_choices.append(
-                (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
-            )
-
+        # Add current site to VLANs query params
         site = getattr(self.instance.parent, 'site', None)
         if site is not None:
-
-            # Add non-grouped site VLANs
-            site_vlans = VLAN.objects.filter(site=site, group=None)
-            vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
-
-            # Add grouped site VLANs
-            for group in VLANGroup.objects.filter(site=site):
-                site_group_vlans = VLAN.objects.filter(group=group)
-                vlan_choices.append((
-                    '{} / {}'.format(group.site.name, group.name),
-                    [(vlan.pk, vlan) for vlan in site_group_vlans]
-                ))
-
-        self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
-        self.fields['tagged_vlans'].choices = vlan_choices
+            # Add current site to VLANs query params
+            self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', site.pk)
+            self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', site.pk)
 
     def clean(self):
         super().clean()
@@ -780,7 +763,10 @@ class InterfaceCreateForm(BootstrapMixin, forms.Form):
         widget=APISelect(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
     )
     tagged_vlans = forms.ModelMultipleChoiceField(
@@ -789,7 +775,10 @@ class InterfaceCreateForm(BootstrapMixin, forms.Form):
         widget=APISelectMultiple(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
     )
     tags = TagField(
@@ -803,35 +792,11 @@ def __init__(self, *args, **kwargs):
             pk=self.initial.get('virtual_machine') or self.data.get('virtual_machine')
         )
 
-        # Limit VLAN choices to those in: global vlans, global groups, the current site's group, the current site
-        vlan_choices = []
-        global_vlans = VLAN.objects.filter(site=None, group=None)
-        vlan_choices.append(
-            ('Global', [(vlan.pk, vlan) for vlan in global_vlans])
-        )
-        for group in VLANGroup.objects.filter(site=None):
-            global_group_vlans = VLAN.objects.filter(group=group)
-            vlan_choices.append(
-                (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
-            )
-
         site = getattr(virtual_machine.cluster, 'site', None)
         if site is not None:
-
-            # Add non-grouped site VLANs
-            site_vlans = VLAN.objects.filter(site=site, group=None)
-            vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
-
-            # Add grouped site VLANs
-            for group in VLANGroup.objects.filter(site=site):
-                site_group_vlans = VLAN.objects.filter(group=group)
-                vlan_choices.append((
-                    '{} / {}'.format(group.site.name, group.name),
-                    [(vlan.pk, vlan) for vlan in site_group_vlans]
-                ))
-
-        self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
-        self.fields['tagged_vlans'].choices = vlan_choices
+            # Add current site to VLANs query params
+            self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', site.pk)
+            self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', site.pk)
 
 
 class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
@@ -868,7 +833,10 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
         widget=APISelect(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
     )
     tagged_vlans = forms.ModelMultipleChoiceField(
@@ -877,7 +845,10 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
         widget=APISelectMultiple(
             api_url="/api/ipam/vlans/",
             display_field='display_name',
-            full=True
+            full=True,
+            additional_query_params={
+                'site_id': 'null',
+            },
         )
     )
 
@@ -893,35 +864,11 @@ def __init__(self, *args, **kwargs):
         if 'virtual_machine' in self.initial:
             parent_obj = VirtualMachine.objects.filter(pk=self.initial['virtual_machine']).first()
 
-            # Limit VLAN choices to global VLANs, VLANs in global groups, the current site's group, the current site
-            vlan_choices = []
-            global_vlans = VLAN.objects.filter(site=None, group=None)
-            vlan_choices.append(
-                ('Global', [(vlan.pk, vlan) for vlan in global_vlans])
-            )
-            for group in VLANGroup.objects.filter(site=None):
-                global_group_vlans = VLAN.objects.filter(group=group)
-                vlan_choices.append(
-                    (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
-                )
-            if parent_obj.cluster is not None:
-                site = getattr(parent_obj.cluster, 'site', None)
-                if site is not None:
-
-                    # Add non-grouped site VLANs
-                    site_vlans = VLAN.objects.filter(site=site, group=None)
-                    vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
-
-                    # Add grouped site VLANs
-                    for group in VLANGroup.objects.filter(site=site):
-                        site_group_vlans = VLAN.objects.filter(group=group)
-                        vlan_choices.append((
-                            '{} / {}'.format(group.site.name, group.name),
-                            [(vlan.pk, vlan) for vlan in site_group_vlans]
-                        ))
-
-            self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
-            self.fields['tagged_vlans'].choices = vlan_choices
+            site = getattr(parent_obj.cluster, 'site', None)
+            if site is not None:
+                # Add current site to VLANs query params
+                self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', site.pk)
+                self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', site.pk)
 
 
 #


Test patch: