jeremystretch 4 лет назад
Родитель
Сommit
01f791a44e

+ 2 - 1
netbox/netbox/navigation_menu.py

@@ -168,6 +168,7 @@ CONNECTIONS_MENU = Menu(
             label='Connections',
             items=(
                 get_model_item('dcim', 'cable', 'Cables', actions=['import']),
+                get_model_item('wireless', 'wirelesslink', 'Wirelesss Links', actions=['import']),
                 MenuItem(
                     link='dcim:interface_connections_list',
                     link_text='Interface Connections',
@@ -196,7 +197,7 @@ WIRELESS_MENU = Menu(
             label='Wireless',
             items=(
                 get_model_item('wireless', 'wirelesslan', 'Wireless LANs'),
-                get_model_item('wireless', 'wirelesslink', 'Wirelesss Links', actions=['import']),
+                get_model_item('wireless', 'wirelesslangroup', 'Wireless LAN Groups'),
             ),
         ),
     ),

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

@@ -13,6 +13,16 @@
                         <th scope="row">SSID</th>
                         <td>{{ object.ssid }}</td>
                     </tr>
+                    <tr>
+                        <td>Group</td>
+                        <td>
+                            {% if object.group %}
+                                <a href="{{ object.group.get_absolute_url }}">{{ object.group }}</a>
+                            {% else %}
+                                <span class="text-muted">None</span>
+                            {% endif %}
+                        </td>
+                    </tr>
                     <tr>
                         <th scope="row">Description</th>
                         <td>{{ object.description|placeholder }}</td>

+ 72 - 0
netbox/templates/wireless/wirelesslangroup.html

@@ -0,0 +1,72 @@
+{% extends 'generic/object.html' %}
+{% load helpers %}
+{% load plugins %}
+
+{% block breadcrumbs %}
+  {{ block.super }}
+  {% for group in object.get_ancestors %}
+    <li class="breadcrumb-item"><a href="{% url 'wireless:wirelesslangroup_list' %}?parent_id={{ group.pk }}">{{ group }}</a></li>
+  {% endfor %}
+{% endblock %}
+
+{% block content %}
+<div class="row mb-3">
+	<div class="col col-md-6">
+    <div class="card">
+      <h5 class="card-header">Wireless LAN Group</h5>
+      <div class="card-body">
+        <table class="table table-hover attr-table">
+          <tr>
+            <th scope="row">Name</th>
+            <td>{{ object.name }}</td>
+          </tr>
+          <tr>
+            <th scope="row">Description</th>
+            <td>{{ object.description|placeholder }}</td>
+          </tr>
+          <tr>
+            <th scope="row">Parent</th>
+            <td>
+              {% if object.parent %}
+                <a href="{{ object.parent.get_absolute_url }}">{{ object.parent }}</a>
+              {% else %}
+                <span class="text-muted">&mdash;</span>
+              {% endif %}
+            </td>
+          </tr>
+          <tr>
+            <th scope="row">Wireless LANs</th>
+            <td>
+              <a href="{% url 'wireless:wirelesslan_list' %}?group_id={{ object.pk }}">{{ wirelesslans_table.rows|length }}</a>
+            </td>
+          </tr>
+        </table>
+      </div>
+    </div>
+    {% plugin_left_page object %}
+  </div>
+	<div class="col col-md-6">
+    {% include 'inc/custom_fields_panel.html' %}
+    {% plugin_right_page object %}
+	</div>
+</div>
+<div class="row mb-3">
+	<div class="col col-md-12">
+    <div class="card">
+      <div class="card-header">Wireless LANs</div>
+      <div class="card-body">
+        {% include 'inc/table.html' with table=wirelesslans_table %}
+      </div>
+      {% if perms.wireless.add_wirelesslan %}
+        <div class="card-footer text-end noprint">
+          <a href="{% url 'wireless:wirelesslan_add' %}?group={{ object.pk }}" class="btn btn-sm btn-primary">
+            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Wireless LAN
+          </a>
+        </div>
+      {% endif %}
+      </div>
+      {% include 'inc/paginator.html' with paginator=wirelesslans_table.paginator page=wirelesslans_table.page %}
+      {% plugin_full_width_page object %}
+  </div>
+</div>
+{% endblock %}

+ 11 - 0
netbox/wireless/api/nested_serializers.py

@@ -5,10 +5,21 @@ from wireless.models import *
 
 __all__ = (
     'NestedWirelessLANSerializer',
+    'NestedWirelessLANGroupSerializer',
     'NestedWirelessLinkSerializer',
 )
 
 
+class NestedWirelessLANGroupSerializer(WritableNestedSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslangroup-detail')
+    wirelesslan_count = serializers.IntegerField(read_only=True)
+    _depth = serializers.IntegerField(source='level', read_only=True)
+
+    class Meta:
+        model = WirelessLANGroup
+        fields = ['id', 'url', 'display', 'name', 'slug', 'wirelesslan_count', '_depth']
+
+
 class NestedWirelessLANSerializer(WritableNestedSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail')
 

+ 14 - 1
netbox/wireless/api/serializers.py

@@ -4,7 +4,7 @@ from dcim.choices import LinkStatusChoices
 from dcim.api.serializers import NestedInterfaceSerializer
 from ipam.api.serializers import NestedVLANSerializer
 from netbox.api import ChoiceField
-from netbox.api.serializers import PrimaryModelSerializer
+from netbox.api.serializers import NestedGroupModelSerializer, PrimaryModelSerializer
 from wireless.models import *
 from .nested_serializers import *
 
@@ -14,6 +14,19 @@ __all__ = (
 )
 
 
+class WirelessLANGroupSerializer(NestedGroupModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslangroup-detail')
+    parent = NestedWirelessLANGroupSerializer(required=False, allow_null=True)
+    wirelesslan_count = serializers.IntegerField(read_only=True)
+
+    class Meta:
+        model = WirelessLANGroup
+        fields = [
+            'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'custom_fields', 'created', 'last_updated',
+            'wirelesslan_count', '_depth',
+        ]
+
+
 class WirelessLANSerializer(PrimaryModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail')
     vlan = NestedVLANSerializer(required=False, allow_null=True)

+ 1 - 0
netbox/wireless/api/urls.py

@@ -5,6 +5,7 @@ from . import views
 router = OrderedDefaultRouter()
 router.APIRootView = views.WirelessRootView
 
+router.register('wireless-lan-groupss', views.WirelessLANGroupViewSet)
 router.register('wireless-lans', views.WirelessLANViewSet)
 router.register('wireless-links', views.WirelessLinkViewSet)
 

+ 12 - 0
netbox/wireless/api/views.py

@@ -14,6 +14,18 @@ class WirelessRootView(APIRootView):
         return 'Wireless'
 
 
+class WirelessLANGroupViewSet(CustomFieldModelViewSet):
+    queryset = WirelessLANGroup.objects.add_related_count(
+        WirelessLANGroup.objects.all(),
+        WirelessLAN,
+        'group',
+        'wirelesslan_count',
+        cumulative=True
+    )
+    serializer_class = serializers.WirelessLANGroupSerializer
+    filterset_class = filtersets.WirelessLANGroupFilterSet
+
+
 class WirelessLANViewSet(CustomFieldModelViewSet):
     queryset = WirelessLAN.objects.prefetch_related('vlan', 'tags')
     serializer_class = serializers.WirelessLANSerializer

+ 17 - 1
netbox/wireless/filtersets.py

@@ -3,15 +3,31 @@ from django.db.models import Q
 
 from dcim.choices import LinkStatusChoices
 from extras.filters import TagFilter
-from netbox.filtersets import PrimaryModelFilterSet
+from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
 from .models import *
 
 __all__ = (
     'WirelessLANFilterSet',
+    'WirelessLANGroupFilterSet',
     'WirelessLinkFilterSet',
 )
 
 
+class WirelessLANGroupFilterSet(OrganizationalModelFilterSet):
+    parent_id = django_filters.ModelMultipleChoiceFilter(
+        queryset=WirelessLANGroup.objects.all()
+    )
+    parent = django_filters.ModelMultipleChoiceFilter(
+        field_name='parent__slug',
+        queryset=WirelessLANGroup.objects.all(),
+        to_field_name='slug'
+    )
+
+    class Meta:
+        model = WirelessLANGroup
+        fields = ['id', 'name', 'slug', 'description']
+
+
 class WirelessLANFilterSet(PrimaryModelFilterSet):
     q = django_filters.CharFilter(
         method='search',

+ 24 - 1
netbox/wireless/forms/bulk_edit.py

@@ -9,15 +9,38 @@ from wireless.models import *
 
 __all__ = (
     'WirelessLANBulkEditForm',
+    'WirelessLANGroupBulkEditForm',
     'WirelessLinkBulkEditForm',
 )
 
 
+class WirelessLANGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
+    pk = forms.ModelMultipleChoiceField(
+        queryset=WirelessLANGroup.objects.all(),
+        widget=forms.MultipleHiddenInput
+    )
+    parent = DynamicModelChoiceField(
+        queryset=WirelessLANGroup.objects.all(),
+        required=False
+    )
+    description = forms.CharField(
+        max_length=200,
+        required=False
+    )
+
+    class Meta:
+        nullable_fields = ['parent', 'description']
+
+
 class WirelessLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
     pk = forms.ModelMultipleChoiceField(
         queryset=WirelessLAN.objects.all(),
         widget=forms.MultipleHiddenInput
     )
+    group = DynamicModelChoiceField(
+        queryset=WirelessLANGroup.objects.all(),
+        required=False
+    )
     vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
@@ -31,7 +54,7 @@ class WirelessLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldMode
     )
 
     class Meta:
-        nullable_fields = ['vlan', 'ssid', 'description']
+        nullable_fields = ['ssid', 'group', 'vlan', 'description']
 
 
 class WirelessLinkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):

+ 23 - 2
netbox/wireless/forms/bulk_import.py

@@ -2,16 +2,37 @@ from dcim.choices import LinkStatusChoices
 from dcim.models import Interface
 from extras.forms import CustomFieldModelCSVForm
 from ipam.models import VLAN
-from utilities.forms import CSVChoiceField, CSVModelChoiceField
+from utilities.forms import CSVChoiceField, CSVModelChoiceField, SlugField
 from wireless.models import *
 
 __all__ = (
     'WirelessLANCSVForm',
+    'WirelessLANGroupCSVForm',
     'WirelessLinkCSVForm',
 )
 
 
+class WirelessLANGroupCSVForm(CustomFieldModelCSVForm):
+    parent = CSVModelChoiceField(
+        queryset=WirelessLANGroup.objects.all(),
+        required=False,
+        to_field_name='name',
+        help_text='Parent group'
+    )
+    slug = SlugField()
+
+    class Meta:
+        model = WirelessLANGroup
+        fields = ('name', 'slug', 'parent', 'description')
+
+
 class WirelessLANCSVForm(CustomFieldModelCSVForm):
+    group = CSVModelChoiceField(
+        queryset=WirelessLANGroup.objects.all(),
+        required=False,
+        to_field_name='name',
+        help_text='Assigned group'
+    )
     vlan = CSVModelChoiceField(
         queryset=VLAN.objects.all(),
         to_field_name='name',
@@ -20,7 +41,7 @@ class WirelessLANCSVForm(CustomFieldModelCSVForm):
 
     class Meta:
         model = WirelessLAN
-        fields = ('ssid', 'description', 'vlan')
+        fields = ('ssid', 'group', 'description', 'vlan')
 
 
 class WirelessLinkCSVForm(CustomFieldModelCSVForm):

+ 28 - 2
netbox/wireless/forms/filtersets.py

@@ -3,19 +3,38 @@ from django.utils.translation import gettext as _
 
 from dcim.choices import LinkStatusChoices
 from extras.forms import CustomFieldModelFilterForm
-from utilities.forms import add_blank_choice, BootstrapMixin, StaticSelect, TagFilterField
+from utilities.forms import (
+    add_blank_choice, BootstrapMixin, DynamicModelMultipleChoiceField, StaticSelect, TagFilterField,
+)
 from wireless.models import *
 
 __all__ = (
     'WirelessLANFilterForm',
+    'WirelessLANGroupFilterForm',
     'WirelessLinkFilterForm',
 )
 
 
+class WirelessLANGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
+    model = WirelessLANGroup
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
+    parent_id = DynamicModelMultipleChoiceField(
+        queryset=WirelessLANGroup.objects.all(),
+        required=False,
+        label=_('Parent group'),
+        fetch_trigger='open'
+    )
+
+
 class WirelessLANFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
     model = WirelessLAN
     field_groups = [
-        ['q', 'tag'],
+        ('q', 'tag'),
+        ('group_id',),
     ]
     q = forms.CharField(
         required=False,
@@ -26,6 +45,13 @@ class WirelessLANFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
         required=False,
         label='SSID'
     )
+    group_id = DynamicModelMultipleChoiceField(
+        queryset=WirelessLANGroup.objects.all(),
+        required=False,
+        null_option='None',
+        label=_('Group'),
+        fetch_trigger='open'
+    )
     tag = TagFilterField(model)
 
 

+ 24 - 3
netbox/wireless/forms/models.py

@@ -2,16 +2,37 @@ from dcim.models import Interface
 from extras.forms import CustomFieldModelForm
 from extras.models import Tag
 from ipam.models import VLAN
-from utilities.forms import BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect
+from utilities.forms import (
+    BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, StaticSelect,
+)
 from wireless.models import *
 
 __all__ = (
     'WirelessLANForm',
+    'WirelessLANGroupForm',
     'WirelessLinkForm',
 )
 
 
+class WirelessLANGroupForm(BootstrapMixin, CustomFieldModelForm):
+    parent = DynamicModelChoiceField(
+        queryset=WirelessLANGroup.objects.all(),
+        required=False
+    )
+    slug = SlugField()
+
+    class Meta:
+        model = WirelessLANGroup
+        fields = [
+            'parent', 'name', 'slug', 'description',
+        ]
+
+
 class WirelessLANForm(BootstrapMixin, CustomFieldModelForm):
+    group = DynamicModelChoiceField(
+        queryset=WirelessLANGroup.objects.all(),
+        required=False
+    )
     vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
         required=False
@@ -24,10 +45,10 @@ class WirelessLANForm(BootstrapMixin, CustomFieldModelForm):
     class Meta:
         model = WirelessLAN
         fields = [
-            'ssid', 'description', 'vlan', 'tags',
+            'ssid', 'group', 'description', 'vlan', 'tags',
         ]
         fieldsets = (
-            ('Wireless LAN', ('ssid', 'description', 'tags')),
+            ('Wireless LAN', ('ssid', 'group', 'description', 'tags')),
             ('VLAN', ('vlan',)),
         )
 

+ 6 - 0
netbox/wireless/graphql/schema.py

@@ -7,3 +7,9 @@ from .types import *
 class WirelessQuery(graphene.ObjectType):
     wirelesslan = ObjectField(WirelessLANType)
     wirelesslan_list = ObjectListField(WirelessLANType)
+
+    wirelesslangroup = ObjectField(WirelessLANGroupType)
+    wirelesslangroup_list = ObjectListField(WirelessLANGroupType)
+
+    wirelesslink = ObjectField(WirelessLinkType)
+    wirelesslink_list = ObjectListField(WirelessLinkType)

+ 9 - 0
netbox/wireless/graphql/types.py

@@ -3,10 +3,19 @@ from netbox.graphql.types import ObjectType
 
 __all__ = (
     'WirelessLANType',
+    'WirelessLANGroupType',
     'WirelessLinkType',
 )
 
 
+class WirelessLANGroupType(ObjectType):
+
+    class Meta:
+        model = models.WirelessLANGroup
+        fields = '__all__'
+        filterset_class = filtersets.WirelessLANGroupFilterSet
+
+
 class WirelessLANType(ObjectType):
 
     class Meta:

+ 23 - 2
netbox/wireless/migrations/0001_wireless.py

@@ -1,8 +1,7 @@
-# Generated by Django 3.2.8 on 2021-10-13 13:44
-
 import django.core.serializers.json
 from django.db import migrations, models
 import django.db.models.deletion
+import mptt.fields
 import taggit.managers
 
 
@@ -17,6 +16,27 @@ class Migration(migrations.Migration):
     ]
 
     operations = [
+        migrations.CreateModel(
+            name='WirelessLANGroup',
+            fields=[
+                ('created', models.DateField(auto_now_add=True, null=True)),
+                ('last_updated', models.DateTimeField(auto_now=True, null=True)),
+                ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
+                ('id', models.BigAutoField(primary_key=True, serialize=False)),
+                ('name', models.CharField(max_length=100, unique=True)),
+                ('slug', models.SlugField(max_length=100, unique=True)),
+                ('description', models.CharField(blank=True, max_length=200)),
+                ('lft', models.PositiveIntegerField(editable=False)),
+                ('rght', models.PositiveIntegerField(editable=False)),
+                ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
+                ('level', models.PositiveIntegerField(editable=False)),
+                ('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='wireless.wirelesslangroup')),
+            ],
+            options={
+                'ordering': ('name', 'pk'),
+                'unique_together': {('parent', 'name')},
+            },
+        ),
         migrations.CreateModel(
             name='WirelessLAN',
             fields=[
@@ -25,6 +45,7 @@ class Migration(migrations.Migration):
                 ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder)),
                 ('id', models.BigAutoField(primary_key=True, serialize=False)),
                 ('ssid', models.CharField(max_length=32)),
+                ('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='wireless_lans', to='wireless.wirelesslangroup')),
                 ('description', models.CharField(blank=True, max_length=200)),
                 ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
                 ('vlan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ipam.vlan')),

+ 47 - 2
netbox/wireless/models.py

@@ -1,20 +1,58 @@
 from django.core.exceptions import ValidationError
 from django.db import models
 from django.urls import reverse
+from mptt.models import MPTTModel, TreeForeignKey
 
 from dcim.choices import LinkStatusChoices
 from dcim.constants import WIRELESS_IFACE_TYPES
 from extras.utils import extras_features
-from netbox.models import BigIDModel, PrimaryModel
+from netbox.models import BigIDModel, NestedGroupModel, PrimaryModel
 from utilities.querysets import RestrictedQuerySet
 from .constants import SSID_MAX_LENGTH
 
 __all__ = (
     'WirelessLAN',
+    'WirelessLANGroup',
     'WirelessLink',
 )
 
 
+@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
+class WirelessLANGroup(NestedGroupModel):
+    """
+    A nested grouping of WirelessLANs
+    """
+    name = models.CharField(
+        max_length=100,
+        unique=True
+    )
+    slug = models.SlugField(
+        max_length=100,
+        unique=True
+    )
+    parent = TreeForeignKey(
+        to='self',
+        on_delete=models.CASCADE,
+        related_name='children',
+        blank=True,
+        null=True,
+        db_index=True
+    )
+    description = models.CharField(
+        max_length=200,
+        blank=True
+    )
+
+    class Meta:
+        ordering = ('name', 'pk')
+        unique_together = (
+            ('parent', 'name')
+        )
+
+    def get_absolute_url(self):
+        return reverse('wireless:wirelesslangroup', args=[self.pk])
+
+
 @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class WirelessLAN(PrimaryModel):
     """
@@ -24,6 +62,13 @@ class WirelessLAN(PrimaryModel):
         max_length=SSID_MAX_LENGTH,
         verbose_name='SSID'
     )
+    group = models.ForeignKey(
+        to='wireless.WirelessLANGroup',
+        on_delete=models.SET_NULL,
+        related_name='wireless_lans',
+        blank=True,
+        null=True
+    )
     vlan = models.ForeignKey(
         to='ipam.VLAN',
         on_delete=models.PROTECT,
@@ -100,7 +145,7 @@ class WirelessLink(PrimaryModel):
 
     objects = RestrictedQuerySet.as_manager()
 
-    clone_fields = ('ssid', 'status')
+    clone_fields = ('ssid', 'group', 'status')
 
     class Meta:
         ordering = ['pk']

+ 22 - 1
netbox/wireless/tables.py

@@ -1,14 +1,35 @@
 import django_tables2 as tables
 
+from utilities.tables import (
+    BaseTable, ButtonsColumn, ChoiceFieldColumn, LinkedCountColumn, MPTTColumn, TagColumn, ToggleColumn,
+)
 from .models import *
-from utilities.tables import BaseTable, ChoiceFieldColumn, TagColumn, ToggleColumn
 
 __all__ = (
     'WirelessLANTable',
+    'WirelessLANGroupTable',
     'WirelessLinkTable',
 )
 
 
+class WirelessLANGroupTable(BaseTable):
+    pk = ToggleColumn()
+    name = MPTTColumn(
+        linkify=True
+    )
+    wirelesslan_count = LinkedCountColumn(
+        viewname='wireless:wirelesslan_list',
+        url_params={'group_id': 'pk'},
+        verbose_name='Wireless LANs'
+    )
+    actions = ButtonsColumn(WirelessLANGroup)
+
+    class Meta(BaseTable.Meta):
+        model = WirelessLANGroup
+        fields = ('pk', 'name', 'wirelesslan_count', 'description', 'slug', 'actions')
+        default_columns = ('pk', 'name', 'wirelesslan_count', 'description', 'actions')
+
+
 class WirelessLANTable(BaseTable):
     pk = ToggleColumn()
     ssid = tables.Column(

+ 11 - 0
netbox/wireless/urls.py

@@ -7,6 +7,17 @@ from .models import *
 app_name = 'wireless'
 urlpatterns = (
 
+    # Wireless LAN groups
+    path('wireless-lan-groups/', views.WirelessLANGroupListView.as_view(), name='wirelesslangroup_list'),
+    path('wireless-lan-groups/add/', views.WirelessLANGroupEditView.as_view(), name='wirelesslangroup_add'),
+    path('wireless-lan-groups/import/', views.WirelessLANGroupBulkImportView.as_view(), name='wirelesslangroup_import'),
+    path('wireless-lan-groups/edit/', views.WirelessLANGroupBulkEditView.as_view(), name='wirelesslangroup_bulk_edit'),
+    path('wireless-lan-groups/delete/', views.WirelessLANGroupBulkDeleteView.as_view(), name='wirelesslangroup_bulk_delete'),
+    path('wireless-lan-groups/<int:pk>/', views.WirelessLANGroupView.as_view(), name='wirelesslangroup'),
+    path('wireless-lan-groups/<int:pk>/edit/', views.WirelessLANGroupEditView.as_view(), name='wirelesslangroup_edit'),
+    path('wireless-lan-groups/<int:pk>/delete/', views.WirelessLANGroupDeleteView.as_view(), name='wirelesslangroup_delete'),
+    path('wireless-lan-groups/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='wirelesslangroup_changelog', kwargs={'model': WirelessLANGroup}),
+
     # Wireless LANs
     path('wireless-lans/', views.WirelessLANListView.as_view(), name='wirelesslan_list'),
     path('wireless-lans/add/', views.WirelessLANEditView.as_view(), name='wirelesslan_add'),

+ 73 - 0
netbox/wireless/views.py

@@ -1,8 +1,81 @@
 from netbox.views import generic
+from utilities.tables import paginate_table
 from . import filtersets, forms, tables
 from .models import *
 
 
+#
+# Wireless LAN groups
+#
+
+class WirelessLANGroupListView(generic.ObjectListView):
+    queryset = WirelessLANGroup.objects.add_related_count(
+        WirelessLANGroup.objects.all(),
+        WirelessLAN,
+        'group',
+        'wirelesslan_count',
+        cumulative=True
+    )
+    filterset = filtersets.WirelessLANGroupFilterSet
+    filterset_form = forms.WirelessLANGroupFilterForm
+    table = tables.WirelessLANGroupTable
+
+
+class WirelessLANGroupView(generic.ObjectView):
+    queryset = WirelessLANGroup.objects.all()
+
+    def get_extra_context(self, request, instance):
+        wirelesslans = WirelessLAN.objects.restrict(request.user, 'view').filter(
+            group=instance
+        )
+        wirelesslans_table = tables.WirelessLANTable(wirelesslans, exclude=('group',))
+        paginate_table(wirelesslans_table, request)
+
+        return {
+            'wirelesslans_table': wirelesslans_table,
+        }
+
+
+class WirelessLANGroupEditView(generic.ObjectEditView):
+    queryset = WirelessLANGroup.objects.all()
+    model_form = forms.WirelessLANGroupForm
+
+
+class WirelessLANGroupDeleteView(generic.ObjectDeleteView):
+    queryset = WirelessLANGroup.objects.all()
+
+
+class WirelessLANGroupBulkImportView(generic.BulkImportView):
+    queryset = WirelessLANGroup.objects.all()
+    model_form = forms.WirelessLANGroupCSVForm
+    table = tables.WirelessLANGroupTable
+
+
+class WirelessLANGroupBulkEditView(generic.BulkEditView):
+    queryset = WirelessLANGroup.objects.add_related_count(
+        WirelessLANGroup.objects.all(),
+        WirelessLAN,
+        'group',
+        'wirelesslan_count',
+        cumulative=True
+    )
+    filterset = filtersets.WirelessLANGroupFilterSet
+    table = tables.WirelessLANGroupTable
+    form = forms.WirelessLANGroupBulkEditForm
+
+
+class WirelessLANGroupBulkDeleteView(generic.BulkDeleteView):
+    queryset = WirelessLANGroup.objects.add_related_count(
+        WirelessLANGroup.objects.all(),
+        WirelessLAN,
+        'group',
+        'wirelesslan_count',
+        cumulative=True
+    )
+    filterset = filtersets.WirelessLANGroupFilterSet
+    table = tables.WirelessLANGroupTable
+
+
 #
 # Wireless LANs
 #