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

10711 Add Scope to WirelessLAN (#17877)

* 7699 Add Scope to Cluster

* 7699 Serializer

* 7699 filterset

* 7699 bulk_edit

* 7699 bulk_import

* 7699 model_form

* 7699 graphql, tables

* 7699 fixes

* 7699 fixes

* 7699 fixes

* 7699 fixes

* 7699 fix tests

* 7699 fix graphql tests for clusters reference

* 7699 fix dcim tests

* 7699 fix ipam tests

* 7699 fix tests

* 7699 use mixin for model

* 7699 change mixin name

* 7699 scope form

* 7699 scope form

* 7699 scoped form, fitlerset

* 7699 review changes

* 7699 move ScopedFilterset

* 7699 move CachedScopeMixin

* 7699 review changes

* 10711 Add Scope to WirelessLAN

* 10711 Add Scope to WirelessLAN

* 10711 Add Scope to WirelessLAN

* 10711 Add Scope to WirelessLAN

* 10711 Add Scope to WirelessLAN

* 7699 review changes

* 7699 refactor mixins

* 7699 _sitegroup -> _site_group

* 7699 update docstring

* fix model

* remove old constants, update filtersets

* 10711 fix GraphQL

* 10711 fix API

* 10711 add tests

* 10711 review changes

* 10711 add tests

* 10711 add scope to detail template

* 10711 add api test

* Extend CSV test data

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
Arthur Hanson 1 год назад
Родитель
Сommit
812ce8471a

+ 4 - 0
docs/models/wireless/wirelesslan.md

@@ -43,3 +43,7 @@ The security cipher used to apply wireless authentication. Options include:
 ### Pre-Shared Key
 
 The security key configured on each client to grant access to the secured wireless LAN. This applies only to certain authentication types.
+
+### Scope
+
+The [region](../dcim/region.md), [site](../dcim/site.md), [site group](../dcim/sitegroup.md) or [location](../dcim/location.md) with which this wireless LAN is associated.

+ 8 - 0
netbox/templates/wireless/wirelesslan.html

@@ -22,6 +22,14 @@
           <th scope="row">{% trans "Status" %}</th>
           <td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
         </tr>
+        <tr>
+          <th scope="row">{% trans "Scope" %}</th>
+          {% if object.scope %}
+            <td>{{ object.scope|linkify }} ({% trans object.scope_type.name %})</td>
+          {% else %}
+            <td>{{ ''|placeholder }}</td>
+          {% endif %}
+        </tr>
         <tr>
           <th scope="row">{% trans "Description" %}</th>
           <td>{{ object.description|placeholder }}</td>

+ 25 - 3
netbox/wireless/api/serializers_/wirelesslans.py

@@ -1,9 +1,13 @@
 from rest_framework import serializers
 
+from dcim.constants import LOCATION_SCOPE_TYPES
+from django.contrib.contenttypes.models import ContentType
+from drf_spectacular.utils import extend_schema_field
 from ipam.api.serializers_.vlans import VLANSerializer
-from netbox.api.fields import ChoiceField
+from netbox.api.fields import ChoiceField, ContentTypeField
 from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
 from tenancy.api.serializers_.tenants import TenantSerializer
+from utilities.api import get_serializer_for_model
 from wireless.choices import *
 from wireless.models import WirelessLAN, WirelessLANGroup
 from .nested import NestedWirelessLANGroupSerializer
@@ -34,12 +38,30 @@ class WirelessLANSerializer(NetBoxModelSerializer):
     tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True)
     auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True)
+    scope_type = ContentTypeField(
+        queryset=ContentType.objects.filter(
+            model__in=LOCATION_SCOPE_TYPES
+        ),
+        allow_null=True,
+        required=False,
+        default=None
+    )
+    scope_id = serializers.IntegerField(allow_null=True, required=False, default=None)
+    scope = serializers.SerializerMethodField(read_only=True)
 
     class Meta:
         model = WirelessLAN
         fields = [
-            'id', 'url', 'display_url', 'display', 'ssid', 'description', 'group', 'status', 'vlan', 'tenant',
-            'auth_type', 'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'custom_fields',
+            'id', 'url', 'display_url', 'display', 'ssid', 'description', 'group', 'status', 'vlan', 'scope_type', 'scope_id', 'scope',
+            'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'custom_fields',
             'created', 'last_updated',
         ]
         brief_fields = ('id', 'url', 'display', 'ssid', 'description')
+
+    @extend_schema_field(serializers.JSONField(allow_null=True))
+    def get_scope(self, obj):
+        if obj.scope_id is None:
+            return None
+        serializer = get_serializer_for_model(obj.scope)
+        context = {'request': self.context['request']}
+        return serializer(obj.scope, nested=True, context=context).data

+ 3 - 2
netbox/wireless/filtersets.py

@@ -2,6 +2,7 @@ import django_filters
 from django.db.models import Q
 
 from dcim.choices import LinkStatusChoices
+from dcim.filtersets import ScopedFilterSet
 from dcim.models import Interface
 from ipam.models import VLAN
 from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet
@@ -43,7 +44,7 @@ class WirelessLANGroupFilterSet(OrganizationalModelFilterSet):
         fields = ('id', 'name', 'slug', 'description')
 
 
-class WirelessLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
+class WirelessLANFilterSet(NetBoxModelFilterSet, ScopedFilterSet, TenancyFilterSet):
     group_id = TreeNodeMultipleChoiceFilter(
         queryset=WirelessLANGroup.objects.all(),
         field_name='group',
@@ -74,7 +75,7 @@ class WirelessLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
 
     class Meta:
         model = WirelessLAN
-        fields = ('id', 'ssid', 'auth_psk', 'description')
+        fields = ('id', 'ssid', 'auth_psk', 'scope_id', 'description')
 
     def search(self, queryset, name, value):
         if not value.strip():

+ 4 - 2
netbox/wireless/forms/bulk_edit.py

@@ -2,6 +2,7 @@ from django import forms
 from django.utils.translation import gettext_lazy as _
 
 from dcim.choices import LinkStatusChoices
+from dcim.forms.mixins import ScopedBulkEditForm
 from ipam.models import VLAN
 from netbox.choices import *
 from netbox.forms import NetBoxModelBulkEditForm
@@ -39,7 +40,7 @@ class WirelessLANGroupBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('parent', 'description')
 
 
-class WirelessLANBulkEditForm(NetBoxModelBulkEditForm):
+class WirelessLANBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm):
     status = forms.ChoiceField(
         label=_('Status'),
         choices=add_blank_choice(WirelessLANStatusChoices),
@@ -89,10 +90,11 @@ class WirelessLANBulkEditForm(NetBoxModelBulkEditForm):
     model = WirelessLAN
     fieldsets = (
         FieldSet('group', 'ssid', 'status', 'vlan', 'tenant', 'description'),
+        FieldSet('scope_type', 'scope', name=_('Scope')),
         FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
     )
     nullable_fields = (
-        'ssid', 'group', 'vlan', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'comments',
+        'ssid', 'group', 'vlan', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'scope', 'comments',
     )
 
 

+ 8 - 4
netbox/wireless/forms/bulk_import.py

@@ -1,12 +1,13 @@
 from django.utils.translation import gettext_lazy as _
 
 from dcim.choices import LinkStatusChoices
+from dcim.forms.mixins import ScopedImportForm
 from dcim.models import Interface
 from ipam.models import VLAN
 from netbox.choices import *
 from netbox.forms import NetBoxModelImportForm
 from tenancy.models import Tenant
-from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField
+from utilities.forms.fields import CSVChoiceField,  CSVModelChoiceField, SlugField
 from wireless.choices import *
 from wireless.models import *
 
@@ -32,7 +33,7 @@ class WirelessLANGroupImportForm(NetBoxModelImportForm):
         fields = ('name', 'slug', 'parent', 'description', 'tags')
 
 
-class WirelessLANImportForm(NetBoxModelImportForm):
+class WirelessLANImportForm(ScopedImportForm, NetBoxModelImportForm):
     group = CSVModelChoiceField(
         label=_('Group'),
         queryset=WirelessLANGroup.objects.all(),
@@ -75,9 +76,12 @@ class WirelessLANImportForm(NetBoxModelImportForm):
     class Meta:
         model = WirelessLAN
         fields = (
-            'ssid', 'group', 'status', 'vlan', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'description',
-            'comments', 'tags',
+            'ssid', 'group', 'status', 'vlan', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'scope_type', 'scope_id',
+            'description', 'comments', 'tags',
         )
+        labels = {
+            'scope_id': _('Scope ID'),
+        }
 
 
 class WirelessLinkImportForm(NetBoxModelImportForm):

+ 27 - 0
netbox/wireless/forms/filtersets.py

@@ -2,6 +2,7 @@ from django import forms
 from django.utils.translation import gettext_lazy as _
 
 from dcim.choices import LinkStatusChoices
+from dcim.models import Location, Region, Site, SiteGroup
 from netbox.choices import *
 from netbox.forms import NetBoxModelFilterSetForm
 from tenancy.forms import TenancyFilterForm
@@ -33,6 +34,7 @@ class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     fieldsets = (
         FieldSet('q', 'filter_id', 'tag'),
         FieldSet('ssid', 'group_id', 'status', name=_('Attributes')),
+        FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Scope')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
     )
@@ -65,6 +67,31 @@ class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
         label=_('Pre-shared key'),
         required=False
     )
+    region_id = DynamicModelMultipleChoiceField(
+        queryset=Region.objects.all(),
+        required=False,
+        label=_('Region')
+    )
+    site_group_id = DynamicModelMultipleChoiceField(
+        queryset=SiteGroup.objects.all(),
+        required=False,
+        label=_('Site group')
+    )
+    site_id = DynamicModelMultipleChoiceField(
+        queryset=Site.objects.all(),
+        required=False,
+        null_option='None',
+        query_params={
+            'region_id': '$region_id',
+            'site_group_id': '$site_group_id',
+        },
+        label=_('Site')
+    )
+    location_id = DynamicModelMultipleChoiceField(
+        queryset=Location.objects.all(),
+        required=False,
+        label=_('Location')
+    )
     tag = TagFilterField(model)
 
 

+ 4 - 2
netbox/wireless/forms/model_forms.py

@@ -2,6 +2,7 @@ from django.forms import PasswordInput
 from django.utils.translation import gettext_lazy as _
 
 from dcim.models import Device, Interface, Location, Site
+from dcim.forms.mixins import ScopedForm
 from ipam.models import VLAN
 from netbox.forms import NetBoxModelForm
 from tenancy.forms import TenancyForm
@@ -35,7 +36,7 @@ class WirelessLANGroupForm(NetBoxModelForm):
         ]
 
 
-class WirelessLANForm(TenancyForm, NetBoxModelForm):
+class WirelessLANForm(ScopedForm, TenancyForm, NetBoxModelForm):
     group = DynamicModelChoiceField(
         label=_('Group'),
         queryset=WirelessLANGroup.objects.all(),
@@ -51,6 +52,7 @@ class WirelessLANForm(TenancyForm, NetBoxModelForm):
 
     fieldsets = (
         FieldSet('ssid', 'group', 'vlan', 'status', 'description', 'tags', name=_('Wireless LAN')),
+        FieldSet('scope_type', 'scope', name=_('Scope')),
         FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
         FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
     )
@@ -59,7 +61,7 @@ class WirelessLANForm(TenancyForm, NetBoxModelForm):
         model = WirelessLAN
         fields = [
             'ssid', 'group', 'status', 'vlan', 'tenant_group', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk',
-            'description', 'comments', 'tags',
+            'scope_type', 'description', 'comments', 'tags',
         ]
         widgets = {
             'auth_psk': PasswordInput(

+ 11 - 2
netbox/wireless/graphql/types.py

@@ -1,4 +1,4 @@
-from typing import Annotated, List
+from typing import Annotated, List, Union
 
 import strawberry
 import strawberry_django
@@ -28,7 +28,7 @@ class WirelessLANGroupType(OrganizationalObjectType):
 
 @strawberry_django.type(
     models.WirelessLAN,
-    fields='__all__',
+    exclude=('scope_type', 'scope_id', '_location', '_region', '_site', '_site_group'),
     filters=WirelessLANFilter
 )
 class WirelessLANType(NetBoxObjectType):
@@ -38,6 +38,15 @@ class WirelessLANType(NetBoxObjectType):
 
     interfaces: List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]
 
+    @strawberry_django.field
+    def scope(self) -> Annotated[Union[
+        Annotated["LocationType", strawberry.lazy('dcim.graphql.types')],
+        Annotated["RegionType", strawberry.lazy('dcim.graphql.types')],
+        Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')],
+        Annotated["SiteType", strawberry.lazy('dcim.graphql.types')],
+    ], strawberry.union("WirelessLANScopeType")] | None:
+        return self.scope
+
 
 @strawberry_django.type(
     models.WirelessLink,

+ 77 - 0
netbox/wireless/migrations/0011_wirelesslan__location_wirelesslan__region_and_more.py

@@ -0,0 +1,77 @@
+# Generated by Django 5.0.9 on 2024-11-04 16:00
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('contenttypes', '0002_remove_content_type_name'),
+        ('dcim', '0196_qinq_svlan'),
+        ('wireless', '0010_charfield_null_choices'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='wirelesslan',
+            name='_location',
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name='_%(class)ss',
+                to='dcim.location',
+            ),
+        ),
+        migrations.AddField(
+            model_name='wirelesslan',
+            name='_region',
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name='_%(class)ss',
+                to='dcim.region',
+            ),
+        ),
+        migrations.AddField(
+            model_name='wirelesslan',
+            name='_site',
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name='_%(class)ss',
+                to='dcim.site',
+            ),
+        ),
+        migrations.AddField(
+            model_name='wirelesslan',
+            name='_site_group',
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name='_%(class)ss',
+                to='dcim.sitegroup',
+            ),
+        ),
+        migrations.AddField(
+            model_name='wirelesslan',
+            name='scope_id',
+            field=models.PositiveBigIntegerField(blank=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='wirelesslan',
+            name='scope_type',
+            field=models.ForeignKey(
+                blank=True,
+                limit_choices_to=models.Q(('model__in', ('region', 'sitegroup', 'site', 'location'))),
+                null=True,
+                on_delete=django.db.models.deletion.PROTECT,
+                related_name='+',
+                to='contenttypes.contenttype',
+            ),
+        ),
+    ]

+ 3 - 2
netbox/wireless/models.py

@@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
 
 from dcim.choices import LinkStatusChoices
 from dcim.constants import WIRELESS_IFACE_TYPES
+from dcim.models.mixins import CachedScopeMixin
 from netbox.models import NestedGroupModel, PrimaryModel
 from netbox.models.mixins import DistanceMixin
 from .choices import *
@@ -71,7 +72,7 @@ class WirelessLANGroup(NestedGroupModel):
         verbose_name_plural = _('wireless LAN groups')
 
 
-class WirelessLAN(WirelessAuthenticationBase, PrimaryModel):
+class WirelessLAN(WirelessAuthenticationBase, CachedScopeMixin, PrimaryModel):
     """
     A wireless network formed among an arbitrary number of access point and clients.
     """
@@ -107,7 +108,7 @@ class WirelessLAN(WirelessAuthenticationBase, PrimaryModel):
         null=True
     )
 
-    clone_fields = ('ssid', 'group', 'tenant', 'description')
+    clone_fields = ('ssid', 'group', 'scope_type', 'scope_id', 'tenant', 'description')
 
     class Meta:
         ordering = ('ssid', 'pk')

+ 8 - 1
netbox/wireless/tables/wirelesslan.py

@@ -51,6 +51,13 @@ class WirelessLANTable(TenancyColumnsMixin, NetBoxTable):
     status = columns.ChoiceFieldColumn(
         verbose_name=_('Status'),
     )
+    scope_type = columns.ContentTypeColumn(
+        verbose_name=_('Scope Type'),
+    )
+    scope = tables.Column(
+        verbose_name=_('Scope'),
+        linkify=True
+    )
     interface_count = tables.Column(
         verbose_name=_('Interfaces')
     )
@@ -65,7 +72,7 @@ class WirelessLANTable(TenancyColumnsMixin, NetBoxTable):
         model = WirelessLAN
         fields = (
             'pk', 'ssid', 'group', 'status', 'tenant', 'tenant_group', 'vlan', 'interface_count', 'auth_type',
-            'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'created', 'last_updated',
+            'auth_cipher', 'auth_psk', 'scope', 'scope_type', 'description', 'comments', 'tags', 'created', 'last_updated',
         )
         default_columns = ('pk', 'ssid', 'group', 'status', 'description', 'vlan', 'auth_type', 'interface_count')
 

+ 9 - 1
netbox/wireless/tests/test_api.py

@@ -1,7 +1,7 @@
 from django.urls import reverse
 
 from dcim.choices import InterfaceTypeChoices
-from dcim.models import Interface
+from dcim.models import Interface, Site
 from tenancy.models import Tenant
 from utilities.testing import APITestCase, APIViewTestCases, create_test_device
 from wireless.choices import *
@@ -53,6 +53,12 @@ class WirelessLANTest(APIViewTestCases.APIViewTestCase):
     @classmethod
     def setUpTestData(cls):
 
+        sites = (
+            Site(name='Site 1', slug='site-1'),
+            Site(name='Site 2', slug='site-2'),
+        )
+        Site.objects.bulk_create(sites)
+
         tenants = (
             Tenant(name='Tenant 1', slug='tenant-1'),
             Tenant(name='Tenant 2', slug='tenant-2'),
@@ -94,6 +100,8 @@ class WirelessLANTest(APIViewTestCases.APIViewTestCase):
                 'status': WirelessLANStatusChoices.STATUS_DISABLED,
                 'tenant': tenants[0].pk,
                 'auth_type': WirelessAuthTypeChoices.TYPE_WPA_ENTERPRISE,
+                'scope_type': 'dcim.site',
+                'scope_id': sites[1].pk,
             },
         ]
 

+ 72 - 6
netbox/wireless/tests/test_filtersets.py

@@ -1,7 +1,7 @@
 from django.test import TestCase
 
 from dcim.choices import InterfaceTypeChoices, LinkStatusChoices
-from dcim.models import Interface
+from dcim.models import Interface, Location, Region, Site, SiteGroup
 from ipam.models import VLAN
 from netbox.choices import DistanceUnitChoices
 from tenancy.models import Tenant
@@ -110,6 +110,36 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
         )
         VLAN.objects.bulk_create(vlans)
 
+        regions = (
+            Region(name='Test Region 1', slug='test-region-1'),
+            Region(name='Test Region 2', slug='test-region-2'),
+            Region(name='Test Region 3', slug='test-region-3'),
+        )
+        for r in regions:
+            r.save()
+
+        site_groups = (
+            SiteGroup(name='Site Group 1', slug='site-group-1'),
+            SiteGroup(name='Site Group 2', slug='site-group-2'),
+            SiteGroup(name='Site Group 3', slug='site-group-3'),
+        )
+        for site_group in site_groups:
+            site_group.save()
+
+        sites = (
+            Site(name='Test Site 1', slug='test-site-1', region=regions[0], group=site_groups[0]),
+            Site(name='Test Site 2', slug='test-site-2', region=regions[1], group=site_groups[1]),
+            Site(name='Test Site 3', slug='test-site-3', region=regions[2], group=site_groups[2]),
+        )
+        Site.objects.bulk_create(sites)
+
+        locations = (
+            Location(name='Location 1', slug='location-1', site=sites[0]),
+            Location(name='Location 2', slug='location-2', site=sites[2]),
+        )
+        for location in locations:
+            location.save()
+
         tenants = (
             Tenant(name='Tenant 1', slug='tenant-1'),
             Tenant(name='Tenant 2', slug='tenant-2'),
@@ -127,7 +157,8 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
                 auth_type=WirelessAuthTypeChoices.TYPE_OPEN,
                 auth_cipher=WirelessAuthCipherChoices.CIPHER_AUTO,
                 auth_psk='PSK1',
-                description='foobar1'
+                description='foobar1',
+                scope=sites[0]
             ),
             WirelessLAN(
                 ssid='WLAN2',
@@ -138,7 +169,8 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
                 auth_type=WirelessAuthTypeChoices.TYPE_WEP,
                 auth_cipher=WirelessAuthCipherChoices.CIPHER_TKIP,
                 auth_psk='PSK2',
-                description='foobar2'
+                description='foobar2',
+                scope=locations[0]
             ),
             WirelessLAN(
                 ssid='WLAN3',
@@ -149,12 +181,14 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
                 auth_type=WirelessAuthTypeChoices.TYPE_WPA_PERSONAL,
                 auth_cipher=WirelessAuthCipherChoices.CIPHER_AES,
                 auth_psk='PSK3',
-                description='foobar3'
+                description='foobar3',
+                scope=locations[1]
             ),
         )
-        WirelessLAN.objects.bulk_create(wireless_lans)
+        for wireless_lan in wireless_lans:
+            wireless_lan.save()
 
-        device = create_test_device('Device 1')
+        device = create_test_device('Device 1', site=sites[0])
         interfaces = (
             Interface(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_80211N),
             Interface(device=device, name='Interface 2', type=InterfaceTypeChoices.TYPE_80211N),
@@ -217,6 +251,38 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
+    def test_region(self):
+        regions = Region.objects.all()[:2]
+        params = {'region_id': [regions[0].pk, regions[1].pk]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+        params = {'region': [regions[0].slug, regions[1].slug]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_site_group(self):
+        site_groups = SiteGroup.objects.all()[:2]
+        params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+        params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_site(self):
+        sites = Site.objects.all()[:2]
+        params = {'site_id': [sites[0].pk, sites[1].pk]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+        params = {'site': [sites[0].slug, sites[1].slug]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_location(self):
+        locations = Location.objects.all()[:1]
+        params = {'location_id': [locations[0].pk,]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+        params = {'location': [locations[0].slug,]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+    def test_scope_type(self):
+        params = {'scope_type': 'dcim.location'}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
 
 class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
     queryset = WirelessLink.objects.all()

+ 14 - 5
netbox/wireless/tests/test_views.py

@@ -1,7 +1,8 @@
+from django.contrib.contenttypes.models import ContentType
 from wireless.choices import *
 from wireless.models import *
 from dcim.choices import InterfaceTypeChoices, LinkStatusChoices
-from dcim.models import Interface
+from dcim.models import Interface, Site
 from netbox.choices import DistanceUnitChoices
 from tenancy.models import Tenant
 from utilities.testing import ViewTestCases, create_tags, create_test_device
@@ -56,6 +57,12 @@ class WirelessLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
     @classmethod
     def setUpTestData(cls):
 
+        sites = (
+            Site(name='Site 1', slug='site-1'),
+            Site(name='Site 2', slug='site-2'),
+        )
+        Site.objects.bulk_create(sites)
+
         tenants = (
             Tenant(name='Tenant 1', slug='tenant-1'),
             Tenant(name='Tenant 2', slug='tenant-2'),
@@ -98,15 +105,17 @@ class WirelessLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             'ssid': 'WLAN2',
             'group': groups[1].pk,
             'status': WirelessLANStatusChoices.STATUS_DISABLED,
+            'scope_type': ContentType.objects.get_for_model(Site).pk,
+            'scope': sites[1].pk,
             'tenant': tenants[1].pk,
             'tags': [t.pk for t in tags],
         }
 
         cls.csv_data = (
-            "group,ssid,status,tenant",
-            f"Wireless LAN Group 2,WLAN4,{WirelessLANStatusChoices.STATUS_ACTIVE},{tenants[0].name}",
-            f"Wireless LAN Group 2,WLAN5,{WirelessLANStatusChoices.STATUS_DISABLED},{tenants[1].name}",
-            f"Wireless LAN Group 2,WLAN6,{WirelessLANStatusChoices.STATUS_RESERVED},{tenants[2].name}",
+            "group,ssid,status,tenant,scope_type,scope_id",
+            f"Wireless LAN Group 2,WLAN4,{WirelessLANStatusChoices.STATUS_ACTIVE},{tenants[0].name},,",
+            f"Wireless LAN Group 2,WLAN5,{WirelessLANStatusChoices.STATUS_DISABLED},{tenants[1].name},dcim.site,{sites[0].pk}",
+            f"Wireless LAN Group 2,WLAN6,{WirelessLANStatusChoices.STATUS_RESERVED},{tenants[2].name},dcim.site,{sites[1].pk}",
         )
 
         cls.csv_update_data = (