diff --git a/docs/models/wireless/wirelesslan.md b/docs/models/wireless/wirelesslan.md index 0f50fa75f..a448c42a2 100644 --- a/docs/models/wireless/wirelesslan.md +++ b/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. diff --git a/netbox/wireless/api/serializers_/wirelesslans.py b/netbox/wireless/api/serializers_/wirelesslans.py index 6c5deeb26..b9632e20e 100644 --- a/netbox/wireless/api/serializers_/wirelesslans.py +++ b/netbox/wireless/api/serializers_/wirelesslans.py @@ -34,12 +34,14 @@ 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_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', + 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ] brief_fields = ('id', 'url', 'display', 'ssid', 'description') diff --git a/netbox/wireless/filtersets.py b/netbox/wireless/filtersets.py index 537b2ec5c..5a4195e6c 100644 --- a/netbox/wireless/filtersets.py +++ b/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(): diff --git a/netbox/wireless/forms/bulk_edit.py b/netbox/wireless/forms/bulk_edit.py index c8b378104..09fd88f58 100644 --- a/netbox/wireless/forms/bulk_edit.py +++ b/netbox/wireless/forms/bulk_edit.py @@ -1,16 +1,19 @@ from django import forms +from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext_lazy as _ from dcim.choices import LinkStatusChoices +from dcim.models import Site from ipam.models import VLAN from netbox.choices import * from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import add_blank_choice -from utilities.forms.fields import CommentField, DynamicModelChoiceField +from utilities.forms.fields import CommentField, ContentTypeChoiceField, DynamicModelChoiceField from utilities.forms.rendering import FieldSet +from utilities.forms.widgets import HTMXSelect from wireless.choices import * -from wireless.constants import SSID_MAX_LENGTH +from wireless.constants import WIRELESSLAN_SCOPE_TYPES, SSID_MAX_LENGTH from wireless.models import * __all__ = ( @@ -79,6 +82,19 @@ class WirelessLANBulkEditForm(NetBoxModelBulkEditForm): required=False, label=_('Pre-shared key') ) + scope_type = ContentTypeChoiceField( + queryset=ContentType.objects.filter(model__in=WIRELESSLAN_SCOPE_TYPES), + widget=HTMXSelect(method='post', attrs={'hx-select': '#form_fields'}), + required=False, + label=_('Scope type') + ) + scope = DynamicModelChoiceField( + label=_('Scope'), + queryset=Site.objects.none(), # Initial queryset + required=False, + disabled=True, + selector=True + ) description = forms.CharField( label=_('Description'), max_length=200, @@ -89,10 +105,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', ) diff --git a/netbox/wireless/forms/bulk_import.py b/netbox/wireless/forms/bulk_import.py index cff3e49af..5c1041f54 100644 --- a/netbox/wireless/forms/bulk_import.py +++ b/netbox/wireless/forms/bulk_import.py @@ -1,3 +1,4 @@ +from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext_lazy as _ from dcim.choices import LinkStatusChoices @@ -6,8 +7,9 @@ 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, CSVContentTypeField, CSVModelChoiceField, SlugField from wireless.choices import * +from wireless.constants import WIRELESSLAN_SCOPE_TYPES from wireless.models import * __all__ = ( @@ -71,13 +73,21 @@ class WirelessLANImportForm(NetBoxModelImportForm): required=False, help_text=_('Authentication cipher') ) + scope_type = CSVContentTypeField( + queryset=ContentType.objects.filter(model__in=WIRELESSLAN_SCOPE_TYPES), + required=False, + label=_('Scope type (app & model)') + ) 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): diff --git a/netbox/wireless/forms/model_forms.py b/netbox/wireless/forms/model_forms.py index 7c2594271..92db2a5db 100644 --- a/netbox/wireless/forms/model_forms.py +++ b/netbox/wireless/forms/model_forms.py @@ -1,3 +1,4 @@ +from django.contrib.contenttypes.models import ContentType from django.forms import PasswordInput from django.utils.translation import gettext_lazy as _ @@ -5,8 +6,10 @@ from dcim.models import Device, Interface, Location, Site from ipam.models import VLAN from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm -from utilities.forms.fields import CommentField, DynamicModelChoiceField, SlugField +from utilities.forms.fields import CommentField, ContentTypeChoiceField, DynamicModelChoiceField, SlugField from utilities.forms.rendering import FieldSet, InlineFields +from utilities.forms.widgets import HTMXSelect +from wireless.constants import WIRELESSLAN_SCOPE_TYPES from wireless.models import * __all__ = ( @@ -47,10 +50,24 @@ class WirelessLANForm(TenancyForm, NetBoxModelForm): selector=True, label=_('VLAN') ) + scope_type = ContentTypeChoiceField( + queryset=ContentType.objects.filter(model__in=WIRELESSLAN_SCOPE_TYPES), + widget=HTMXSelect(), + required=False, + label=_('Scope type') + ) + scope = DynamicModelChoiceField( + label=_('Scope'), + queryset=Site.objects.none(), # Initial queryset + required=False, + disabled=True, + selector=True + ) comments = CommentField() 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 +76,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( diff --git a/netbox/wireless/graphql/types.py b/netbox/wireless/graphql/types.py index b24525fbe..031dd09ac 100644 --- a/netbox/wireless/graphql/types.py +++ b/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 @@ -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, diff --git a/netbox/wireless/tables/wirelesslan.py b/netbox/wireless/tables/wirelesslan.py index 87ad4ac51..40f52f8a5 100644 --- a/netbox/wireless/tables/wirelesslan.py +++ b/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')