Преглед на файлове

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 година
родител
ревизия
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
 ### 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.
 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>
           <th scope="row">{% trans "Status" %}</th>
           <td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
           <td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
         </tr>
         </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>
         <tr>
           <th scope="row">{% trans "Description" %}</th>
           <th scope="row">{% trans "Description" %}</th>
           <td>{{ object.description|placeholder }}</td>
           <td>{{ object.description|placeholder }}</td>

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

@@ -1,9 +1,13 @@
 from rest_framework import serializers
 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 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 netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
 from tenancy.api.serializers_.tenants import TenantSerializer
 from tenancy.api.serializers_.tenants import TenantSerializer
+from utilities.api import get_serializer_for_model
 from wireless.choices import *
 from wireless.choices import *
 from wireless.models import WirelessLAN, WirelessLANGroup
 from wireless.models import WirelessLAN, WirelessLANGroup
 from .nested import NestedWirelessLANGroupSerializer
 from .nested import NestedWirelessLANGroupSerializer
@@ -34,12 +38,30 @@ class WirelessLANSerializer(NetBoxModelSerializer):
     tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True)
     auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True)
     auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, 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:
     class Meta:
         model = WirelessLAN
         model = WirelessLAN
         fields = [
         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',
             'created', 'last_updated',
         ]
         ]
         brief_fields = ('id', 'url', 'display', 'ssid', 'description')
         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 django.db.models import Q
 
 
 from dcim.choices import LinkStatusChoices
 from dcim.choices import LinkStatusChoices
+from dcim.filtersets import ScopedFilterSet
 from dcim.models import Interface
 from dcim.models import Interface
 from ipam.models import VLAN
 from ipam.models import VLAN
 from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet
 from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet
@@ -43,7 +44,7 @@ class WirelessLANGroupFilterSet(OrganizationalModelFilterSet):
         fields = ('id', 'name', 'slug', 'description')
         fields = ('id', 'name', 'slug', 'description')
 
 
 
 
-class WirelessLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
+class WirelessLANFilterSet(NetBoxModelFilterSet, ScopedFilterSet, TenancyFilterSet):
     group_id = TreeNodeMultipleChoiceFilter(
     group_id = TreeNodeMultipleChoiceFilter(
         queryset=WirelessLANGroup.objects.all(),
         queryset=WirelessLANGroup.objects.all(),
         field_name='group',
         field_name='group',
@@ -74,7 +75,7 @@ class WirelessLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
 
 
     class Meta:
     class Meta:
         model = WirelessLAN
         model = WirelessLAN
-        fields = ('id', 'ssid', 'auth_psk', 'description')
+        fields = ('id', 'ssid', 'auth_psk', 'scope_id', 'description')
 
 
     def search(self, queryset, name, value):
     def search(self, queryset, name, value):
         if not value.strip():
         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 django.utils.translation import gettext_lazy as _
 
 
 from dcim.choices import LinkStatusChoices
 from dcim.choices import LinkStatusChoices
+from dcim.forms.mixins import ScopedBulkEditForm
 from ipam.models import VLAN
 from ipam.models import VLAN
 from netbox.choices import *
 from netbox.choices import *
 from netbox.forms import NetBoxModelBulkEditForm
 from netbox.forms import NetBoxModelBulkEditForm
@@ -39,7 +40,7 @@ class WirelessLANGroupBulkEditForm(NetBoxModelBulkEditForm):
     nullable_fields = ('parent', 'description')
     nullable_fields = ('parent', 'description')
 
 
 
 
-class WirelessLANBulkEditForm(NetBoxModelBulkEditForm):
+class WirelessLANBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm):
     status = forms.ChoiceField(
     status = forms.ChoiceField(
         label=_('Status'),
         label=_('Status'),
         choices=add_blank_choice(WirelessLANStatusChoices),
         choices=add_blank_choice(WirelessLANStatusChoices),
@@ -89,10 +90,11 @@ class WirelessLANBulkEditForm(NetBoxModelBulkEditForm):
     model = WirelessLAN
     model = WirelessLAN
     fieldsets = (
     fieldsets = (
         FieldSet('group', 'ssid', 'status', 'vlan', 'tenant', 'description'),
         FieldSet('group', 'ssid', 'status', 'vlan', 'tenant', 'description'),
+        FieldSet('scope_type', 'scope', name=_('Scope')),
         FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
         FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
     )
     )
     nullable_fields = (
     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 django.utils.translation import gettext_lazy as _
 
 
 from dcim.choices import LinkStatusChoices
 from dcim.choices import LinkStatusChoices
+from dcim.forms.mixins import ScopedImportForm
 from dcim.models import Interface
 from dcim.models import Interface
 from ipam.models import VLAN
 from ipam.models import VLAN
 from netbox.choices import *
 from netbox.choices import *
 from netbox.forms import NetBoxModelImportForm
 from netbox.forms import NetBoxModelImportForm
 from tenancy.models import Tenant
 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.choices import *
 from wireless.models import *
 from wireless.models import *
 
 
@@ -32,7 +33,7 @@ class WirelessLANGroupImportForm(NetBoxModelImportForm):
         fields = ('name', 'slug', 'parent', 'description', 'tags')
         fields = ('name', 'slug', 'parent', 'description', 'tags')
 
 
 
 
-class WirelessLANImportForm(NetBoxModelImportForm):
+class WirelessLANImportForm(ScopedImportForm, NetBoxModelImportForm):
     group = CSVModelChoiceField(
     group = CSVModelChoiceField(
         label=_('Group'),
         label=_('Group'),
         queryset=WirelessLANGroup.objects.all(),
         queryset=WirelessLANGroup.objects.all(),
@@ -75,9 +76,12 @@ class WirelessLANImportForm(NetBoxModelImportForm):
     class Meta:
     class Meta:
         model = WirelessLAN
         model = WirelessLAN
         fields = (
         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):
 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 django.utils.translation import gettext_lazy as _
 
 
 from dcim.choices import LinkStatusChoices
 from dcim.choices import LinkStatusChoices
+from dcim.models import Location, Region, Site, SiteGroup
 from netbox.choices import *
 from netbox.choices import *
 from netbox.forms import NetBoxModelFilterSetForm
 from netbox.forms import NetBoxModelFilterSetForm
 from tenancy.forms import TenancyFilterForm
 from tenancy.forms import TenancyFilterForm
@@ -33,6 +34,7 @@ class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     fieldsets = (
     fieldsets = (
         FieldSet('q', 'filter_id', 'tag'),
         FieldSet('q', 'filter_id', 'tag'),
         FieldSet('ssid', 'group_id', 'status', name=_('Attributes')),
         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('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
         FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
     )
     )
@@ -65,6 +67,31 @@ class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
         label=_('Pre-shared key'),
         label=_('Pre-shared key'),
         required=False
         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)
     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 django.utils.translation import gettext_lazy as _
 
 
 from dcim.models import Device, Interface, Location, Site
 from dcim.models import Device, Interface, Location, Site
+from dcim.forms.mixins import ScopedForm
 from ipam.models import VLAN
 from ipam.models import VLAN
 from netbox.forms import NetBoxModelForm
 from netbox.forms import NetBoxModelForm
 from tenancy.forms import TenancyForm
 from tenancy.forms import TenancyForm
@@ -35,7 +36,7 @@ class WirelessLANGroupForm(NetBoxModelForm):
         ]
         ]
 
 
 
 
-class WirelessLANForm(TenancyForm, NetBoxModelForm):
+class WirelessLANForm(ScopedForm, TenancyForm, NetBoxModelForm):
     group = DynamicModelChoiceField(
     group = DynamicModelChoiceField(
         label=_('Group'),
         label=_('Group'),
         queryset=WirelessLANGroup.objects.all(),
         queryset=WirelessLANGroup.objects.all(),
@@ -51,6 +52,7 @@ class WirelessLANForm(TenancyForm, NetBoxModelForm):
 
 
     fieldsets = (
     fieldsets = (
         FieldSet('ssid', 'group', 'vlan', 'status', 'description', 'tags', name=_('Wireless LAN')),
         FieldSet('ssid', 'group', 'vlan', 'status', 'description', 'tags', name=_('Wireless LAN')),
+        FieldSet('scope_type', 'scope', name=_('Scope')),
         FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
         FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
         FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
         FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
     )
     )
@@ -59,7 +61,7 @@ class WirelessLANForm(TenancyForm, NetBoxModelForm):
         model = WirelessLAN
         model = WirelessLAN
         fields = [
         fields = [
             'ssid', 'group', 'status', 'vlan', 'tenant_group', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk',
             'ssid', 'group', 'status', 'vlan', 'tenant_group', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk',
-            'description', 'comments', 'tags',
+            'scope_type', 'description', 'comments', 'tags',
         ]
         ]
         widgets = {
         widgets = {
             'auth_psk': PasswordInput(
             '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
 import strawberry_django
 import strawberry_django
@@ -28,7 +28,7 @@ class WirelessLANGroupType(OrganizationalObjectType):
 
 
 @strawberry_django.type(
 @strawberry_django.type(
     models.WirelessLAN,
     models.WirelessLAN,
-    fields='__all__',
+    exclude=('scope_type', 'scope_id', '_location', '_region', '_site', '_site_group'),
     filters=WirelessLANFilter
     filters=WirelessLANFilter
 )
 )
 class WirelessLANType(NetBoxObjectType):
 class WirelessLANType(NetBoxObjectType):
@@ -38,6 +38,15 @@ class WirelessLANType(NetBoxObjectType):
 
 
     interfaces: List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]
     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(
 @strawberry_django.type(
     models.WirelessLink,
     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.choices import LinkStatusChoices
 from dcim.constants import WIRELESS_IFACE_TYPES
 from dcim.constants import WIRELESS_IFACE_TYPES
+from dcim.models.mixins import CachedScopeMixin
 from netbox.models import NestedGroupModel, PrimaryModel
 from netbox.models import NestedGroupModel, PrimaryModel
 from netbox.models.mixins import DistanceMixin
 from netbox.models.mixins import DistanceMixin
 from .choices import *
 from .choices import *
@@ -71,7 +72,7 @@ class WirelessLANGroup(NestedGroupModel):
         verbose_name_plural = _('wireless LAN groups')
         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.
     A wireless network formed among an arbitrary number of access point and clients.
     """
     """
@@ -107,7 +108,7 @@ class WirelessLAN(WirelessAuthenticationBase, PrimaryModel):
         null=True
         null=True
     )
     )
 
 
-    clone_fields = ('ssid', 'group', 'tenant', 'description')
+    clone_fields = ('ssid', 'group', 'scope_type', 'scope_id', 'tenant', 'description')
 
 
     class Meta:
     class Meta:
         ordering = ('ssid', 'pk')
         ordering = ('ssid', 'pk')

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

@@ -51,6 +51,13 @@ class WirelessLANTable(TenancyColumnsMixin, NetBoxTable):
     status = columns.ChoiceFieldColumn(
     status = columns.ChoiceFieldColumn(
         verbose_name=_('Status'),
         verbose_name=_('Status'),
     )
     )
+    scope_type = columns.ContentTypeColumn(
+        verbose_name=_('Scope Type'),
+    )
+    scope = tables.Column(
+        verbose_name=_('Scope'),
+        linkify=True
+    )
     interface_count = tables.Column(
     interface_count = tables.Column(
         verbose_name=_('Interfaces')
         verbose_name=_('Interfaces')
     )
     )
@@ -65,7 +72,7 @@ class WirelessLANTable(TenancyColumnsMixin, NetBoxTable):
         model = WirelessLAN
         model = WirelessLAN
         fields = (
         fields = (
             'pk', 'ssid', 'group', 'status', 'tenant', 'tenant_group', 'vlan', 'interface_count', 'auth_type',
             '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')
         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 django.urls import reverse
 
 
 from dcim.choices import InterfaceTypeChoices
 from dcim.choices import InterfaceTypeChoices
-from dcim.models import Interface
+from dcim.models import Interface, Site
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.testing import APITestCase, APIViewTestCases, create_test_device
 from utilities.testing import APITestCase, APIViewTestCases, create_test_device
 from wireless.choices import *
 from wireless.choices import *
@@ -53,6 +53,12 @@ class WirelessLANTest(APIViewTestCases.APIViewTestCase):
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
 
 
+        sites = (
+            Site(name='Site 1', slug='site-1'),
+            Site(name='Site 2', slug='site-2'),
+        )
+        Site.objects.bulk_create(sites)
+
         tenants = (
         tenants = (
             Tenant(name='Tenant 1', slug='tenant-1'),
             Tenant(name='Tenant 1', slug='tenant-1'),
             Tenant(name='Tenant 2', slug='tenant-2'),
             Tenant(name='Tenant 2', slug='tenant-2'),
@@ -94,6 +100,8 @@ class WirelessLANTest(APIViewTestCases.APIViewTestCase):
                 'status': WirelessLANStatusChoices.STATUS_DISABLED,
                 'status': WirelessLANStatusChoices.STATUS_DISABLED,
                 'tenant': tenants[0].pk,
                 'tenant': tenants[0].pk,
                 'auth_type': WirelessAuthTypeChoices.TYPE_WPA_ENTERPRISE,
                 '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 django.test import TestCase
 
 
 from dcim.choices import InterfaceTypeChoices, LinkStatusChoices
 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 ipam.models import VLAN
 from netbox.choices import DistanceUnitChoices
 from netbox.choices import DistanceUnitChoices
 from tenancy.models import Tenant
 from tenancy.models import Tenant
@@ -110,6 +110,36 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
         )
         )
         VLAN.objects.bulk_create(vlans)
         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 = (
         tenants = (
             Tenant(name='Tenant 1', slug='tenant-1'),
             Tenant(name='Tenant 1', slug='tenant-1'),
             Tenant(name='Tenant 2', slug='tenant-2'),
             Tenant(name='Tenant 2', slug='tenant-2'),
@@ -127,7 +157,8 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
                 auth_type=WirelessAuthTypeChoices.TYPE_OPEN,
                 auth_type=WirelessAuthTypeChoices.TYPE_OPEN,
                 auth_cipher=WirelessAuthCipherChoices.CIPHER_AUTO,
                 auth_cipher=WirelessAuthCipherChoices.CIPHER_AUTO,
                 auth_psk='PSK1',
                 auth_psk='PSK1',
-                description='foobar1'
+                description='foobar1',
+                scope=sites[0]
             ),
             ),
             WirelessLAN(
             WirelessLAN(
                 ssid='WLAN2',
                 ssid='WLAN2',
@@ -138,7 +169,8 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
                 auth_type=WirelessAuthTypeChoices.TYPE_WEP,
                 auth_type=WirelessAuthTypeChoices.TYPE_WEP,
                 auth_cipher=WirelessAuthCipherChoices.CIPHER_TKIP,
                 auth_cipher=WirelessAuthCipherChoices.CIPHER_TKIP,
                 auth_psk='PSK2',
                 auth_psk='PSK2',
-                description='foobar2'
+                description='foobar2',
+                scope=locations[0]
             ),
             ),
             WirelessLAN(
             WirelessLAN(
                 ssid='WLAN3',
                 ssid='WLAN3',
@@ -149,12 +181,14 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
                 auth_type=WirelessAuthTypeChoices.TYPE_WPA_PERSONAL,
                 auth_type=WirelessAuthTypeChoices.TYPE_WPA_PERSONAL,
                 auth_cipher=WirelessAuthCipherChoices.CIPHER_AES,
                 auth_cipher=WirelessAuthCipherChoices.CIPHER_AES,
                 auth_psk='PSK3',
                 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 = (
         interfaces = (
             Interface(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_80211N),
             Interface(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_80211N),
             Interface(device=device, name='Interface 2', 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]}
         params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         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):
 class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
     queryset = WirelessLink.objects.all()
     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.choices import *
 from wireless.models import *
 from wireless.models import *
 from dcim.choices import InterfaceTypeChoices, LinkStatusChoices
 from dcim.choices import InterfaceTypeChoices, LinkStatusChoices
-from dcim.models import Interface
+from dcim.models import Interface, Site
 from netbox.choices import DistanceUnitChoices
 from netbox.choices import DistanceUnitChoices
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.testing import ViewTestCases, create_tags, create_test_device
 from utilities.testing import ViewTestCases, create_tags, create_test_device
@@ -56,6 +57,12 @@ class WirelessLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
 
 
+        sites = (
+            Site(name='Site 1', slug='site-1'),
+            Site(name='Site 2', slug='site-2'),
+        )
+        Site.objects.bulk_create(sites)
+
         tenants = (
         tenants = (
             Tenant(name='Tenant 1', slug='tenant-1'),
             Tenant(name='Tenant 1', slug='tenant-1'),
             Tenant(name='Tenant 2', slug='tenant-2'),
             Tenant(name='Tenant 2', slug='tenant-2'),
@@ -98,15 +105,17 @@ class WirelessLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             'ssid': 'WLAN2',
             'ssid': 'WLAN2',
             'group': groups[1].pk,
             'group': groups[1].pk,
             'status': WirelessLANStatusChoices.STATUS_DISABLED,
             'status': WirelessLANStatusChoices.STATUS_DISABLED,
+            'scope_type': ContentType.objects.get_for_model(Site).pk,
+            'scope': sites[1].pk,
             'tenant': tenants[1].pk,
             'tenant': tenants[1].pk,
             'tags': [t.pk for t in tags],
             'tags': [t.pk for t in tags],
         }
         }
 
 
         cls.csv_data = (
         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 = (
         cls.csv_update_data = (