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

Merge pull request #18859 from netbox-community/17602-comments-field-for-nested-models

Closes #17602: adds comments field to NestedGroupModel children
bctiemann 11 месяцев назад
Родитель
Сommit
cd10087b2b
42 измененных файлов с 364 добавлено и 117 удалено
  1. 3 3
      netbox/dcim/api/serializers_/sites.py
  2. 12 11
      netbox/dcim/filtersets.py
  3. 6 3
      netbox/dcim/forms/bulk_edit.py
  4. 6 3
      netbox/dcim/forms/bulk_import.py
  5. 7 3
      netbox/dcim/forms/model_forms.py
  6. 26 0
      netbox/dcim/migrations/0202_location_comments_region_comments_sitegroup_comments.py
  7. 3 0
      netbox/dcim/search.py
  8. 14 5
      netbox/dcim/tables/sites.py
  9. 14 3
      netbox/dcim/tests/test_api.py
  10. 39 5
      netbox/dcim/tests/test_filtersets.py
  11. 34 23
      netbox/dcim/tests/test_views.py
  12. 16 0
      netbox/netbox/filtersets.py
  13. 4 0
      netbox/netbox/models/__init__.py
  14. 1 0
      netbox/templates/dcim/location.html
  15. 1 0
      netbox/templates/dcim/region.html
  16. 1 0
      netbox/templates/dcim/sitegroup.html
  17. 1 0
      netbox/templates/tenancy/contactgroup.html
  18. 1 0
      netbox/templates/tenancy/tenantgroup.html
  19. 1 0
      netbox/templates/wireless/wirelesslangroup.html
  20. 1 1
      netbox/tenancy/api/serializers_/contacts.py
  21. 1 1
      netbox/tenancy/api/serializers_/tenants.py
  22. 3 3
      netbox/tenancy/filtersets.py
  23. 4 2
      netbox/tenancy/forms/bulk_edit.py
  24. 2 2
      netbox/tenancy/forms/bulk_import.py
  25. 4 2
      netbox/tenancy/forms/model_forms.py
  26. 21 0
      netbox/tenancy/migrations/0018_contactgroup_comments_tenantgroup_comments.py
  27. 2 0
      netbox/tenancy/search.py
  28. 5 1
      netbox/tenancy/tables/contacts.py
  29. 5 1
      netbox/tenancy/tables/tenants.py
  30. 18 4
      netbox/tenancy/tests/test_api.py
  31. 28 6
      netbox/tenancy/tests/test_filtersets.py
  32. 22 18
      netbox/tenancy/tests/test_views.py
  33. 1 1
      netbox/wireless/api/serializers_/wirelesslans.py
  34. 2 2
      netbox/wireless/filtersets.py
  35. 2 1
      netbox/wireless/forms/bulk_edit.py
  36. 1 1
      netbox/wireless/forms/bulk_import.py
  37. 2 1
      netbox/wireless/forms/model_forms.py
  38. 16 0
      netbox/wireless/migrations/0014_wirelesslangroup_comments.py
  39. 1 0
      netbox/wireless/search.py
  40. 3 0
      netbox/wireless/tests/test_api.py
  41. 17 2
      netbox/wireless/tests/test_filtersets.py
  42. 13 9
      netbox/wireless/tests/test_views.py

+ 3 - 3
netbox/dcim/api/serializers_/sites.py

@@ -27,7 +27,7 @@ class RegionSerializer(NestedGroupModelSerializer):
         model = Region
         model = Region
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields',
             'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields',
-            'created', 'last_updated', 'site_count', 'prefix_count', '_depth',
+            'created', 'last_updated', 'site_count', 'prefix_count', 'comments', '_depth',
         ]
         ]
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
 
 
@@ -41,7 +41,7 @@ class SiteGroupSerializer(NestedGroupModelSerializer):
         model = SiteGroup
         model = SiteGroup
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields',
             'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields',
-            'created', 'last_updated', 'site_count', 'prefix_count', '_depth',
+            'created', 'last_updated', 'site_count', 'prefix_count', 'comments', '_depth',
         ]
         ]
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
 
 
@@ -93,6 +93,6 @@ class LocationSerializer(NestedGroupModelSerializer):
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'facility',
             'id', 'url', 'display_url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'facility',
             'description', 'tags', 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count',
             'description', 'tags', 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count',
-            'prefix_count', '_depth',
+            'prefix_count', 'comments', '_depth',
         ]
         ]
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth')
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth')

+ 12 - 11
netbox/dcim/filtersets.py

@@ -11,7 +11,8 @@ from ipam.filtersets import PrimaryIPFilterSet
 from ipam.models import ASN, IPAddress, VLANTranslationPolicy, VRF
 from ipam.models import ASN, IPAddress, VLANTranslationPolicy, VRF
 from netbox.choices import ColorChoices
 from netbox.choices import ColorChoices
 from netbox.filtersets import (
 from netbox.filtersets import (
-    BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
+    BaseFilterSet, ChangeLoggedModelFilterSet, NestedGroupModelFilterSet, NetBoxModelFilterSet,
+    OrganizationalModelFilterSet,
 )
 )
 from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
 from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
 from tenancy.models import *
 from tenancy.models import *
@@ -81,7 +82,7 @@ __all__ = (
 )
 )
 
 
 
 
-class RegionFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
+class RegionFilterSet(NestedGroupModelFilterSet, ContactModelFilterSet):
     parent_id = django_filters.ModelMultipleChoiceFilter(
     parent_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         label=_('Parent region (ID)'),
         label=_('Parent region (ID)'),
@@ -111,7 +112,7 @@ class RegionFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
         fields = ('id', 'name', 'slug', 'description')
         fields = ('id', 'name', 'slug', 'description')
 
 
 
 
-class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
+class SiteGroupFilterSet(NestedGroupModelFilterSet, ContactModelFilterSet):
     parent_id = django_filters.ModelMultipleChoiceFilter(
     parent_id = django_filters.ModelMultipleChoiceFilter(
         queryset=SiteGroup.objects.all(),
         queryset=SiteGroup.objects.all(),
         label=_('Parent site group (ID)'),
         label=_('Parent site group (ID)'),
@@ -205,7 +206,7 @@ class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
         return queryset.filter(qs_filter).distinct()
         return queryset.filter(qs_filter).distinct()
 
 
 
 
-class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalModelFilterSet):
+class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, NestedGroupModelFilterSet):
     region_id = TreeNodeMultipleChoiceFilter(
     region_id = TreeNodeMultipleChoiceFilter(
         queryset=Region.objects.all(),
         queryset=Region.objects.all(),
         field_name='site__region',
         field_name='site__region',
@@ -275,13 +276,13 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM
         fields = ('id', 'name', 'slug', 'facility', 'description')
         fields = ('id', 'name', 'slug', 'facility', 'description')
 
 
     def search(self, queryset, name, value):
     def search(self, queryset, name, value):
-        if not value.strip():
-            return queryset
-        return queryset.filter(
-            Q(name__icontains=value) |
-            Q(facility__icontains=value) |
-            Q(description__icontains=value)
-        )
+        # extended in order to include querying on Location.facility
+        queryset = super().search(queryset, name, value)
+
+        if value.strip():
+            queryset = queryset | queryset.model.objects.filter(facility__icontains=value)
+
+        return queryset
 
 
 
 
 class RackRoleFilterSet(OrganizationalModelFilterSet):
 class RackRoleFilterSet(OrganizationalModelFilterSet):

+ 6 - 3
netbox/dcim/forms/bulk_edit.py

@@ -78,12 +78,13 @@ class RegionBulkEditForm(NetBoxModelBulkEditForm):
         max_length=200,
         max_length=200,
         required=False
         required=False
     )
     )
+    comments = CommentField()
 
 
     model = Region
     model = Region
     fieldsets = (
     fieldsets = (
         FieldSet('parent', 'description'),
         FieldSet('parent', 'description'),
     )
     )
-    nullable_fields = ('parent', 'description')
+    nullable_fields = ('parent', 'description', 'comments')
 
 
 
 
 class SiteGroupBulkEditForm(NetBoxModelBulkEditForm):
 class SiteGroupBulkEditForm(NetBoxModelBulkEditForm):
@@ -97,12 +98,13 @@ class SiteGroupBulkEditForm(NetBoxModelBulkEditForm):
         max_length=200,
         max_length=200,
         required=False
         required=False
     )
     )
+    comments = CommentField()
 
 
     model = SiteGroup
     model = SiteGroup
     fieldsets = (
     fieldsets = (
         FieldSet('parent', 'description'),
         FieldSet('parent', 'description'),
     )
     )
-    nullable_fields = ('parent', 'description')
+    nullable_fields = ('parent', 'description', 'comments')
 
 
 
 
 class SiteBulkEditForm(NetBoxModelBulkEditForm):
 class SiteBulkEditForm(NetBoxModelBulkEditForm):
@@ -197,12 +199,13 @@ class LocationBulkEditForm(NetBoxModelBulkEditForm):
         max_length=200,
         max_length=200,
         required=False
         required=False
     )
     )
+    comments = CommentField()
 
 
     model = Location
     model = Location
     fieldsets = (
     fieldsets = (
         FieldSet('site', 'parent', 'status', 'tenant', 'description'),
         FieldSet('site', 'parent', 'status', 'tenant', 'description'),
     )
     )
-    nullable_fields = ('parent', 'tenant', 'description')
+    nullable_fields = ('parent', 'tenant', 'description', 'comments')
 
 
 
 
 class RackRoleBulkEditForm(NetBoxModelBulkEditForm):
 class RackRoleBulkEditForm(NetBoxModelBulkEditForm):

+ 6 - 3
netbox/dcim/forms/bulk_import.py

@@ -68,7 +68,7 @@ class RegionImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         model = Region
         model = Region
-        fields = ('name', 'slug', 'parent', 'description', 'tags')
+        fields = ('name', 'slug', 'parent', 'description', 'tags', 'comments')
 
 
 
 
 class SiteGroupImportForm(NetBoxModelImportForm):
 class SiteGroupImportForm(NetBoxModelImportForm):
@@ -82,7 +82,7 @@ class SiteGroupImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         model = SiteGroup
         model = SiteGroup
-        fields = ('name', 'slug', 'parent', 'description')
+        fields = ('name', 'slug', 'parent', 'description', 'comments', 'tags')
 
 
 
 
 class SiteImportForm(NetBoxModelImportForm):
 class SiteImportForm(NetBoxModelImportForm):
@@ -160,7 +160,10 @@ class LocationImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         model = Location
         model = Location
-        fields = ('site', 'parent', 'name', 'slug', 'status', 'tenant', 'facility', 'description', 'tags')
+        fields = (
+            'site', 'parent', 'name', 'slug', 'status', 'tenant', 'facility', 'description',
+            'tags', 'comments',
+        )
 
 
     def __init__(self, data=None, *args, **kwargs):
     def __init__(self, data=None, *args, **kwargs):
         super().__init__(data, *args, **kwargs)
         super().__init__(data, *args, **kwargs)

+ 7 - 3
netbox/dcim/forms/model_forms.py

@@ -78,6 +78,7 @@ class RegionForm(NetBoxModelForm):
         required=False
         required=False
     )
     )
     slug = SlugField()
     slug = SlugField()
+    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet('parent', 'name', 'slug', 'description', 'tags'),
         FieldSet('parent', 'name', 'slug', 'description', 'tags'),
@@ -86,7 +87,7 @@ class RegionForm(NetBoxModelForm):
     class Meta:
     class Meta:
         model = Region
         model = Region
         fields = (
         fields = (
-            'parent', 'name', 'slug', 'description', 'tags',
+            'parent', 'name', 'slug', 'description', 'tags', 'comments',
         )
         )
 
 
 
 
@@ -97,6 +98,7 @@ class SiteGroupForm(NetBoxModelForm):
         required=False
         required=False
     )
     )
     slug = SlugField()
     slug = SlugField()
+    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet('parent', 'name', 'slug', 'description', 'tags'),
         FieldSet('parent', 'name', 'slug', 'description', 'tags'),
@@ -105,7 +107,7 @@ class SiteGroupForm(NetBoxModelForm):
     class Meta:
     class Meta:
         model = SiteGroup
         model = SiteGroup
         fields = (
         fields = (
-            'parent', 'name', 'slug', 'description', 'tags',
+            'parent', 'name', 'slug', 'description', 'comments', 'tags',
         )
         )
 
 
 
 
@@ -179,6 +181,7 @@ class LocationForm(TenancyForm, NetBoxModelForm):
         }
         }
     )
     )
     slug = SlugField()
     slug = SlugField()
+    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet('site', 'parent', 'name', 'slug', 'status', 'facility', 'description', 'tags', name=_('Location')),
         FieldSet('site', 'parent', 'name', 'slug', 'status', 'facility', 'description', 'tags', name=_('Location')),
@@ -188,7 +191,8 @@ class LocationForm(TenancyForm, NetBoxModelForm):
     class Meta:
     class Meta:
         model = Location
         model = Location
         fields = (
         fields = (
-            'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant', 'facility', 'tags',
+            'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant',
+            'facility', 'tags', 'comments',
         )
         )
 
 
 
 

+ 26 - 0
netbox/dcim/migrations/0202_location_comments_region_comments_sitegroup_comments.py

@@ -0,0 +1,26 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0201_add_power_outlet_status'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='location',
+            name='comments',
+            field=models.TextField(blank=True),
+        ),
+        migrations.AddField(
+            model_name='region',
+            name='comments',
+            field=models.TextField(blank=True),
+        ),
+        migrations.AddField(
+            model_name='sitegroup',
+            name='comments',
+            field=models.TextField(blank=True),
+        ),
+    ]

+ 3 - 0
netbox/dcim/search.py

@@ -144,6 +144,7 @@ class LocationIndex(SearchIndex):
         ('facility', 100),
         ('facility', 100),
         ('slug', 110),
         ('slug', 110),
         ('description', 500),
         ('description', 500),
+        ('comments', 5000),
     )
     )
     display_attrs = ('site', 'status', 'tenant', 'facility', 'description')
     display_attrs = ('site', 'status', 'tenant', 'facility', 'description')
 
 
@@ -317,6 +318,7 @@ class RegionIndex(SearchIndex):
         ('name', 100),
         ('name', 100),
         ('slug', 110),
         ('slug', 110),
         ('description', 500),
         ('description', 500),
+        ('comments', 5000),
     )
     )
     display_attrs = ('parent', 'description')
     display_attrs = ('parent', 'description')
 
 
@@ -343,6 +345,7 @@ class SiteGroupIndex(SearchIndex):
         ('name', 100),
         ('name', 100),
         ('slug', 110),
         ('slug', 110),
         ('description', 500),
         ('description', 500),
+        ('comments', 5000),
     )
     )
     display_attrs = ('parent', 'description')
     display_attrs = ('parent', 'description')
 
 

+ 14 - 5
netbox/dcim/tables/sites.py

@@ -32,12 +32,15 @@ class RegionTable(ContactsColumnMixin, NetBoxTable):
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='dcim:region_list'
         url_name='dcim:region_list'
     )
     )
+    comments = columns.MarkdownColumn(
+        verbose_name=_('Comments'),
+    )
 
 
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = Region
         model = Region
         fields = (
         fields = (
-            'pk', 'id', 'name', 'slug', 'site_count', 'description', 'contacts', 'tags', 'created', 'last_updated',
-            'actions',
+            'pk', 'id', 'name', 'slug', 'site_count', 'description', 'comments', 'contacts', 'tags',
+            'created', 'last_updated', 'actions',
         )
         )
         default_columns = ('pk', 'name', 'site_count', 'description')
         default_columns = ('pk', 'name', 'site_count', 'description')
 
 
@@ -59,12 +62,15 @@ class SiteGroupTable(ContactsColumnMixin, NetBoxTable):
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='dcim:sitegroup_list'
         url_name='dcim:sitegroup_list'
     )
     )
+    comments = columns.MarkdownColumn(
+        verbose_name=_('Comments'),
+    )
 
 
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = SiteGroup
         model = SiteGroup
         fields = (
         fields = (
-            'pk', 'id', 'name', 'slug', 'site_count', 'description', 'contacts', 'tags', 'created', 'last_updated',
-            'actions',
+            'pk', 'id', 'name', 'slug', 'site_count', 'description', 'comments', 'contacts', 'tags',
+            'created', 'last_updated', 'actions',
         )
         )
         default_columns = ('pk', 'name', 'site_count', 'description')
         default_columns = ('pk', 'name', 'site_count', 'description')
 
 
@@ -153,12 +159,15 @@ class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
     actions = columns.ActionsColumn(
     actions = columns.ActionsColumn(
         extra_buttons=LOCATION_BUTTONS
         extra_buttons=LOCATION_BUTTONS
     )
     )
+    comments = columns.MarkdownColumn(
+        verbose_name=_('Comments'),
+    )
 
 
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = Location
         model = Location
         fields = (
         fields = (
             'pk', 'id', 'name', 'site', 'status', 'facility', 'tenant', 'tenant_group', 'rack_count', 'device_count',
             'pk', 'id', 'name', 'site', 'status', 'facility', 'tenant', 'tenant_group', 'rack_count', 'device_count',
-            'description', 'slug', 'contacts', 'tags', 'actions', 'created', 'last_updated',
+            'description', 'slug', 'comments', 'contacts', 'tags', 'actions', 'created', 'last_updated',
         )
         )
         default_columns = (
         default_columns = (
             'pk', 'name', 'site', 'status', 'facility', 'tenant', 'rack_count', 'device_count', 'description'
             'pk', 'name', 'site', 'status', 'facility', 'tenant', 'rack_count', 'device_count', 'description'

+ 14 - 3
netbox/dcim/tests/test_api.py

@@ -74,6 +74,7 @@ class RegionTest(APIViewTestCases.APIViewTestCase):
         {
         {
             'name': 'Region 4',
             'name': 'Region 4',
             'slug': 'region-4',
             'slug': 'region-4',
+            'comments': 'this is region 4, not region 5',
         },
         },
         {
         {
             'name': 'Region 5',
             'name': 'Region 5',
@@ -86,13 +87,14 @@ class RegionTest(APIViewTestCases.APIViewTestCase):
     ]
     ]
     bulk_update_data = {
     bulk_update_data = {
         'description': 'New description',
         'description': 'New description',
+        'comments': 'New comments',
     }
     }
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
 
 
         Region.objects.create(name='Region 1', slug='region-1')
         Region.objects.create(name='Region 1', slug='region-1')
-        Region.objects.create(name='Region 2', slug='region-2')
+        Region.objects.create(name='Region 2', slug='region-2', comments='what in the world is happening?')
         Region.objects.create(name='Region 3', slug='region-3')
         Region.objects.create(name='Region 3', slug='region-3')
 
 
 
 
@@ -103,26 +105,30 @@ class SiteGroupTest(APIViewTestCases.APIViewTestCase):
         {
         {
             'name': 'Site Group 4',
             'name': 'Site Group 4',
             'slug': 'site-group-4',
             'slug': 'site-group-4',
+            'comments': '',
         },
         },
         {
         {
             'name': 'Site Group 5',
             'name': 'Site Group 5',
             'slug': 'site-group-5',
             'slug': 'site-group-5',
+            'comments': 'not actually empty',
         },
         },
         {
         {
             'name': 'Site Group 6',
             'name': 'Site Group 6',
             'slug': 'site-group-6',
             'slug': 'site-group-6',
+            'comments': 'Do I really exist?',
         },
         },
     ]
     ]
     bulk_update_data = {
     bulk_update_data = {
         'description': 'New description',
         'description': 'New description',
+        'comments': 'I do exist!',
     }
     }
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
 
 
         SiteGroup.objects.create(name='Site Group 1', slug='site-group-1')
         SiteGroup.objects.create(name='Site Group 1', slug='site-group-1')
-        SiteGroup.objects.create(name='Site Group 2', slug='site-group-2')
-        SiteGroup.objects.create(name='Site Group 3', slug='site-group-3')
+        SiteGroup.objects.create(name='Site Group 2', slug='site-group-2', comments='')
+        SiteGroup.objects.create(name='Site Group 3', slug='site-group-3', comments='Hi!')
 
 
 
 
 class SiteTest(APIViewTestCases.APIViewTestCase):
 class SiteTest(APIViewTestCases.APIViewTestCase):
@@ -212,12 +218,14 @@ class LocationTest(APIViewTestCases.APIViewTestCase):
                 name='Parent Location 1',
                 name='Parent Location 1',
                 slug='parent-location-1',
                 slug='parent-location-1',
                 status=LocationStatusChoices.STATUS_ACTIVE,
                 status=LocationStatusChoices.STATUS_ACTIVE,
+                comments='First!'
             ),
             ),
             Location.objects.create(
             Location.objects.create(
                 site=sites[1],
                 site=sites[1],
                 name='Parent Location 2',
                 name='Parent Location 2',
                 slug='parent-location-2',
                 slug='parent-location-2',
                 status=LocationStatusChoices.STATUS_ACTIVE,
                 status=LocationStatusChoices.STATUS_ACTIVE,
+                comments='Second!'
             ),
             ),
         )
         )
 
 
@@ -227,6 +235,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase):
             slug='location-1',
             slug='location-1',
             parent=parent_locations[0],
             parent=parent_locations[0],
             status=LocationStatusChoices.STATUS_ACTIVE,
             status=LocationStatusChoices.STATUS_ACTIVE,
+            comments='Third!'
         )
         )
         Location.objects.create(
         Location.objects.create(
             site=sites[0],
             site=sites[0],
@@ -250,6 +259,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase):
                 'site': sites[1].pk,
                 'site': sites[1].pk,
                 'parent': parent_locations[1].pk,
                 'parent': parent_locations[1].pk,
                 'status': LocationStatusChoices.STATUS_PLANNED,
                 'status': LocationStatusChoices.STATUS_PLANNED,
+                'comments': '',
             },
             },
             {
             {
                 'name': 'Test Location 5',
                 'name': 'Test Location 5',
@@ -257,6 +267,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase):
                 'site': sites[1].pk,
                 'site': sites[1].pk,
                 'parent': parent_locations[1].pk,
                 'parent': parent_locations[1].pk,
                 'status': LocationStatusChoices.STATUS_PLANNED,
                 'status': LocationStatusChoices.STATUS_PLANNED,
+                'comments': 'Somebody should check on this location',
             },
             },
             {
             {
                 'name': 'Test Location 6',
                 'name': 'Test Location 6',

+ 39 - 5
netbox/dcim/tests/test_filtersets.py

@@ -67,9 +67,15 @@ class RegionTestCase(TestCase, ChangeLoggedFilterSetTests):
     def setUpTestData(cls):
     def setUpTestData(cls):
 
 
         parent_regions = (
         parent_regions = (
-            Region(name='Region 1', slug='region-1', description='foobar1'),
-            Region(name='Region 2', slug='region-2', description='foobar2'),
-            Region(name='Region 3', slug='region-3', description='foobar3'),
+            Region(
+                name='Region 1', slug='region-1', description='foobar1', comments="There's nothing that",
+            ),
+            Region(
+                name='Region 2', slug='region-2', description='foobar2', comments='a hundred men or more',
+            ),
+            Region(
+                name='Region 3', slug='region-3', description='foobar3', comments='could ever do'
+            ),
         )
         )
         for region in parent_regions:
         for region in parent_regions:
             region.save()
             region.save()
@@ -100,6 +106,13 @@ class RegionTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'q': 'foobar1'}
         params = {'q': 'foobar1'}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
+    def test_q_comments(self):
+        params = {'q': 'there'}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+        params = {'q': 'hundred men could'}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
+
     def test_name(self):
     def test_name(self):
         params = {'name': ['Region 1', 'Region 2']}
         params = {'name': ['Region 1', 'Region 2']}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -148,13 +161,17 @@ class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
             SiteGroup(name='Site Group 2A', slug='site-group-2a', parent=parent_groups[1]),
             SiteGroup(name='Site Group 2A', slug='site-group-2a', parent=parent_groups[1]),
             SiteGroup(name='Site Group 2B', slug='site-group-2b', parent=parent_groups[1]),
             SiteGroup(name='Site Group 2B', slug='site-group-2b', parent=parent_groups[1]),
             SiteGroup(name='Site Group 3A', slug='site-group-3a', parent=parent_groups[2]),
             SiteGroup(name='Site Group 3A', slug='site-group-3a', parent=parent_groups[2]),
-            SiteGroup(name='Site Group 3B', slug='site-group-3b', parent=parent_groups[2]),
+            SiteGroup(
+                name='Site Group 3B', slug='site-group-3b', parent=parent_groups[2], comments='this is a parent group',
+            ),
         )
         )
         for site_group in groups:
         for site_group in groups:
             site_group.save()
             site_group.save()
 
 
         child_groups = (
         child_groups = (
-            SiteGroup(name='Site Group 1A1', slug='site-group-1a1', parent=groups[0]),
+            SiteGroup(
+                name='Site Group 1A1', slug='site-group-1a1', parent=groups[0], comments='this is a child group',
+            ),
             SiteGroup(name='Site Group 1B1', slug='site-group-1b1', parent=groups[1]),
             SiteGroup(name='Site Group 1B1', slug='site-group-1b1', parent=groups[1]),
             SiteGroup(name='Site Group 2A1', slug='site-group-2a1', parent=groups[2]),
             SiteGroup(name='Site Group 2A1', slug='site-group-2a1', parent=groups[2]),
             SiteGroup(name='Site Group 2B1', slug='site-group-2b1', parent=groups[3]),
             SiteGroup(name='Site Group 2B1', slug='site-group-2b1', parent=groups[3]),
@@ -168,6 +185,13 @@ class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'q': 'foobar1'}
         params = {'q': 'foobar1'}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
+    def test_q_comments(self):
+        params = {'q': 'this'}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+        params = {'q': 'child'}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
     def test_name(self):
     def test_name(self):
         params = {'name': ['Site Group 1', 'Site Group 2']}
         params = {'name': ['Site Group 1', 'Site Group 2']}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -401,6 +425,7 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
                 status=LocationStatusChoices.STATUS_PLANNED,
                 status=LocationStatusChoices.STATUS_PLANNED,
                 facility='Facility 1',
                 facility='Facility 1',
                 description='foobar1',
                 description='foobar1',
+                comments='',
             ),
             ),
             Location(
             Location(
                 name='Location 2A',
                 name='Location 2A',
@@ -410,6 +435,7 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
                 status=LocationStatusChoices.STATUS_STAGING,
                 status=LocationStatusChoices.STATUS_STAGING,
                 facility='Facility 2',
                 facility='Facility 2',
                 description='foobar2',
                 description='foobar2',
+                comments='First comment!',
             ),
             ),
             Location(
             Location(
                 name='Location 3A',
                 name='Location 3A',
@@ -419,6 +445,7 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
                 status=LocationStatusChoices.STATUS_DECOMMISSIONING,
                 status=LocationStatusChoices.STATUS_DECOMMISSIONING,
                 facility='Facility 3',
                 facility='Facility 3',
                 description='foobar3',
                 description='foobar3',
+                comments='_This_ is a **bold comment**',
             ),
             ),
         )
         )
         for location in locations:
         for location in locations:
@@ -436,6 +463,13 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'q': 'foobar1'}
         params = {'q': 'foobar1'}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
+    def test_q_comments(self):
+        params = {'q': 'this'}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+        params = {'q': 'comment'}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
     def test_name(self):
     def test_name(self):
         params = {'name': ['Location 1', 'Location 2']}
         params = {'name': ['Location 1', 'Location 2']}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

+ 34 - 23
netbox/dcim/tests/test_views.py

@@ -25,8 +25,10 @@ class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
 
 
         # Create three Regions
         # Create three Regions
         regions = (
         regions = (
-            Region(name='Region 1', slug='region-1'),
-            Region(name='Region 2', slug='region-2'),
+            Region(name='Region 1', slug='region-1', comments=''),
+            Region(
+                name='Region 2', slug='region-2', comments="It's going to take a lot to drag me away from you"
+            ),
             Region(name='Region 3', slug='region-3'),
             Region(name='Region 3', slug='region-3'),
         )
         )
         for region in regions:
         for region in regions:
@@ -40,13 +42,14 @@ class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
             'parent': regions[2].pk,
             'parent': regions[2].pk,
             'description': 'A new region',
             'description': 'A new region',
             'tags': [t.pk for t in tags],
             'tags': [t.pk for t in tags],
+            'comments': 'This comment is really exciting!',
         }
         }
 
 
         cls.csv_data = (
         cls.csv_data = (
-            "name,slug,description",
-            "Region 4,region-4,Fourth region",
-            "Region 5,region-5,Fifth region",
-            "Region 6,region-6,Sixth region",
+            "name,slug,description,comments",
+            "Region 4,region-4,Fourth region,",
+            "Region 5,region-5,Fifth region,hi guys",
+            "Region 6,region-6,Sixth region,bye guys",
         )
         )
 
 
         cls.csv_update_data = (
         cls.csv_update_data = (
@@ -58,6 +61,7 @@ class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
 
 
         cls.bulk_edit_data = {
         cls.bulk_edit_data = {
             'description': 'New description',
             'description': 'New description',
+            'comments': 'This comment is super exciting!!!',
         }
         }
 
 
 
 
@@ -69,7 +73,7 @@ class SiteGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
 
 
         # Create three SiteGroups
         # Create three SiteGroups
         sitegroups = (
         sitegroups = (
-            SiteGroup(name='Site Group 1', slug='site-group-1'),
+            SiteGroup(name='Site Group 1', slug='site-group-1', comments='Still here'),
             SiteGroup(name='Site Group 2', slug='site-group-2'),
             SiteGroup(name='Site Group 2', slug='site-group-2'),
             SiteGroup(name='Site Group 3', slug='site-group-3'),
             SiteGroup(name='Site Group 3', slug='site-group-3'),
         )
         )
@@ -84,24 +88,26 @@ class SiteGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
             'parent': sitegroups[2].pk,
             'parent': sitegroups[2].pk,
             'description': 'A new site group',
             'description': 'A new site group',
             'tags': [t.pk for t in tags],
             'tags': [t.pk for t in tags],
+            'comments': 'still here',
         }
         }
 
 
         cls.csv_data = (
         cls.csv_data = (
-            "name,slug,description",
-            "Site Group 4,site-group-4,Fourth site group",
-            "Site Group 5,site-group-5,Fifth site group",
-            "Site Group 6,site-group-6,Sixth site group",
+            "name,slug,description,comments",
+            "Site Group 4,site-group-4,Fourth site group,",
+            "Site Group 5,site-group-5,Fifth site group,still hear",
+            "Site Group 6,site-group-6,Sixth site group,"
         )
         )
 
 
         cls.csv_update_data = (
         cls.csv_update_data = (
-            "id,name,description",
-            f"{sitegroups[0].pk},Site Group 7,Fourth site group7",
-            f"{sitegroups[1].pk},Site Group 8,Fifth site group8",
-            f"{sitegroups[2].pk},Site Group 0,Sixth site group9",
+            "id,name,description,comments",
+            f"{sitegroups[0].pk},Site Group 7,Fourth site group7,",
+            f"{sitegroups[1].pk},Site Group 8,Fifth site group8,when will it end",
+            f"{sitegroups[2].pk},Site Group 0,Sixth site group9,",
         )
         )
 
 
         cls.bulk_edit_data = {
         cls.bulk_edit_data = {
             'description': 'New description',
             'description': 'New description',
+            'comments': 'the end',
         }
         }
 
 
 
 
@@ -202,6 +208,7 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
                 site=site,
                 site=site,
                 status=LocationStatusChoices.STATUS_ACTIVE,
                 status=LocationStatusChoices.STATUS_ACTIVE,
                 tenant=tenant,
                 tenant=tenant,
+                comments='',
             ),
             ),
             Location(
             Location(
                 name='Location 2',
                 name='Location 2',
@@ -209,6 +216,7 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
                 site=site,
                 site=site,
                 status=LocationStatusChoices.STATUS_ACTIVE,
                 status=LocationStatusChoices.STATUS_ACTIVE,
                 tenant=tenant,
                 tenant=tenant,
+                comments='First comment!',
             ),
             ),
             Location(
             Location(
                 name='Location 3',
                 name='Location 3',
@@ -216,6 +224,7 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
                 site=site,
                 site=site,
                 status=LocationStatusChoices.STATUS_ACTIVE,
                 status=LocationStatusChoices.STATUS_ACTIVE,
                 tenant=tenant,
                 tenant=tenant,
+                comments='_This_ is a **bold comment**',
             ),
             ),
         )
         )
         for location in locations:
         for location in locations:
@@ -232,24 +241,26 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
             'tenant': tenant.pk,
             'tenant': tenant.pk,
             'description': 'A new location',
             'description': 'A new location',
             'tags': [t.pk for t in tags],
             'tags': [t.pk for t in tags],
+            'comments': 'This comment is really boring',
         }
         }
 
 
         cls.csv_data = (
         cls.csv_data = (
-            "site,tenant,name,slug,status,description",
-            "Site 1,Tenant 1,Location 4,location-4,planned,Fourth location",
-            "Site 1,Tenant 1,Location 5,location-5,planned,Fifth location",
-            "Site 1,Tenant 1,Location 6,location-6,planned,Sixth location",
+            "site,tenant,name,slug,status,description,comments",
+            "Site 1,Tenant 1,Location 4,location-4,planned,Fourth location,",
+            "Site 1,Tenant 1,Location 5,location-5,planned,Fifth location,",
+            "Site 1,Tenant 1,Location 6,location-6,planned,Sixth location,hi!",
         )
         )
 
 
         cls.csv_update_data = (
         cls.csv_update_data = (
-            "id,name,description",
-            f"{locations[0].pk},Location 7,Fourth location7",
-            f"{locations[1].pk},Location 8,Fifth location8",
-            f"{locations[2].pk},Location 0,Sixth location9",
+            "id,name,description,comments",
+            f"{locations[0].pk},Location 7,Fourth location7,Useful comment",
+            f"{locations[1].pk},Location 8,Fifth location8,unuseful comment",
+            f"{locations[2].pk},Location 0,Sixth location9,",
         )
         )
 
 
         cls.bulk_edit_data = {
         cls.bulk_edit_data = {
             'description': 'New description',
             'description': 'New description',
+            'comments': 'This comment is also really boring',
         }
         }
 
 
 
 

+ 16 - 0
netbox/netbox/filtersets.py

@@ -329,3 +329,19 @@ class OrganizationalModelFilterSet(NetBoxModelFilterSet):
             models.Q(slug__icontains=value) |
             models.Q(slug__icontains=value) |
             models.Q(description__icontains=value)
             models.Q(description__icontains=value)
         )
         )
+
+
+class NestedGroupModelFilterSet(NetBoxModelFilterSet):
+    """
+    A base FilterSet for models that inherit from NestedGroupModel
+    """
+    def search(self, queryset, name, value):
+        if value.strip():
+            queryset = queryset.filter(
+                models.Q(name__icontains=value) |
+                models.Q(slug__icontains=value) |
+                models.Q(description__icontains=value) |
+                models.Q(comments__icontains=value)
+            )
+
+        return queryset

+ 4 - 0
netbox/netbox/models/__init__.py

@@ -150,6 +150,10 @@ class NestedGroupModel(NetBoxFeatureSet, MPTTModel):
         max_length=200,
         max_length=200,
         blank=True
         blank=True
     )
     )
+    comments = models.TextField(
+        verbose_name=_('comments'),
+        blank=True
+    )
 
 
     objects = TreeManager()
     objects = TreeManager()
 
 

+ 1 - 0
netbox/templates/dcim/location.html

@@ -62,6 +62,7 @@
     </div>
     </div>
     {% include 'inc/panels/tags.html' %}
     {% include 'inc/panels/tags.html' %}
     {% include 'inc/panels/custom_fields.html' %}
     {% include 'inc/panels/custom_fields.html' %}
+    {% include 'inc/panels/comments.html' %}
     {% plugin_left_page object %}
     {% plugin_left_page object %}
   </div>
   </div>
 	<div class="col col-md-6">
 	<div class="col col-md-6">

+ 1 - 0
netbox/templates/dcim/region.html

@@ -41,6 +41,7 @@
     </div>
     </div>
     {% include 'inc/panels/tags.html' %}
     {% include 'inc/panels/tags.html' %}
     {% include 'inc/panels/custom_fields.html' %}
     {% include 'inc/panels/custom_fields.html' %}
+    {% include 'inc/panels/comments.html' %}
     {% plugin_left_page object %}
     {% plugin_left_page object %}
   </div>
   </div>
 	<div class="col col-md-6">
 	<div class="col col-md-6">

+ 1 - 0
netbox/templates/dcim/sitegroup.html

@@ -41,6 +41,7 @@
     </div>
     </div>
     {% include 'inc/panels/tags.html' %}
     {% include 'inc/panels/tags.html' %}
     {% include 'inc/panels/custom_fields.html' %}
     {% include 'inc/panels/custom_fields.html' %}
+    {% include 'inc/panels/comments.html' %}
     {% plugin_left_page object %}
     {% plugin_left_page object %}
   </div>
   </div>
 	<div class="col col-md-6">
 	<div class="col col-md-6">

+ 1 - 0
netbox/templates/tenancy/contactgroup.html

@@ -32,6 +32,7 @@
         </table>
         </table>
       </div>
       </div>
       {% include 'inc/panels/tags.html' %}
       {% include 'inc/panels/tags.html' %}
+      {% include 'inc/panels/comments.html' %}
       {% plugin_left_page object %}
       {% plugin_left_page object %}
     </div>
     </div>
     <div class="col col-md-6">
     <div class="col col-md-6">

+ 1 - 0
netbox/templates/tenancy/tenantgroup.html

@@ -40,6 +40,7 @@
       </table>
       </table>
     </div>
     </div>
     {% include 'inc/panels/tags.html' %}
     {% include 'inc/panels/tags.html' %}
+    {% include 'inc/panels/comments.html' %}
     {% plugin_left_page object %}
     {% plugin_left_page object %}
   </div>
   </div>
 	<div class="col col-md-6">
 	<div class="col col-md-6">

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

@@ -40,6 +40,7 @@
       </table>
       </table>
     </div>
     </div>
     {% include 'inc/panels/tags.html' %}
     {% include 'inc/panels/tags.html' %}
+    {% include 'inc/panels/comments.html' %}
     {% plugin_left_page object %}
     {% plugin_left_page object %}
   </div>
   </div>
 	<div class="col col-md-6">
 	<div class="col col-md-6">

+ 1 - 1
netbox/tenancy/api/serializers_/contacts.py

@@ -26,7 +26,7 @@ class ContactGroupSerializer(NestedGroupModelSerializer):
         model = ContactGroup
         model = ContactGroup
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields',
             'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields',
-            'created', 'last_updated', 'contact_count', '_depth',
+            'created', 'last_updated', 'contact_count', 'comments', '_depth',
         ]
         ]
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'contact_count', '_depth')
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'contact_count', '_depth')
 
 

+ 1 - 1
netbox/tenancy/api/serializers_/tenants.py

@@ -19,7 +19,7 @@ class TenantGroupSerializer(NestedGroupModelSerializer):
         model = TenantGroup
         model = TenantGroup
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields',
             'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields',
-            'created', 'last_updated', 'tenant_count', '_depth',
+            'created', 'last_updated', 'tenant_count', 'comments', '_depth',
         ]
         ]
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'tenant_count', '_depth')
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'tenant_count', '_depth')
 
 

+ 3 - 3
netbox/tenancy/filtersets.py

@@ -2,7 +2,7 @@ import django_filters
 from django.db.models import Q
 from django.db.models import Q
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext as _
 
 
-from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet
+from netbox.filtersets import NestedGroupModelFilterSet, NetBoxModelFilterSet, OrganizationalModelFilterSet
 from utilities.filters import ContentTypeFilter, TreeNodeMultipleChoiceFilter
 from utilities.filters import ContentTypeFilter, TreeNodeMultipleChoiceFilter
 from .models import *
 from .models import *
 
 
@@ -22,7 +22,7 @@ __all__ = (
 # Contacts
 # Contacts
 #
 #
 
 
-class ContactGroupFilterSet(OrganizationalModelFilterSet):
+class ContactGroupFilterSet(NestedGroupModelFilterSet):
     parent_id = django_filters.ModelMultipleChoiceFilter(
     parent_id = django_filters.ModelMultipleChoiceFilter(
         queryset=ContactGroup.objects.all(),
         queryset=ContactGroup.objects.all(),
         label=_('Parent contact group (ID)'),
         label=_('Parent contact group (ID)'),
@@ -168,7 +168,7 @@ class ContactModelFilterSet(django_filters.FilterSet):
 # Tenancy
 # Tenancy
 #
 #
 
 
-class TenantGroupFilterSet(OrganizationalModelFilterSet):
+class TenantGroupFilterSet(NestedGroupModelFilterSet):
     parent_id = django_filters.ModelMultipleChoiceFilter(
     parent_id = django_filters.ModelMultipleChoiceFilter(
         queryset=TenantGroup.objects.all(),
         queryset=TenantGroup.objects.all(),
         label=_('Parent tenant group (ID)'),
         label=_('Parent tenant group (ID)'),

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

@@ -33,9 +33,10 @@ class TenantGroupBulkEditForm(NetBoxModelBulkEditForm):
         max_length=200,
         max_length=200,
         required=False
         required=False
     )
     )
+    comments = CommentField()
 
 
     model = TenantGroup
     model = TenantGroup
-    nullable_fields = ('parent', 'description')
+    nullable_fields = ('parent', 'description', 'comments')
 
 
 
 
 class TenantBulkEditForm(NetBoxModelBulkEditForm):
 class TenantBulkEditForm(NetBoxModelBulkEditForm):
@@ -67,12 +68,13 @@ class ContactGroupBulkEditForm(NetBoxModelBulkEditForm):
         max_length=200,
         max_length=200,
         required=False
         required=False
     )
     )
+    comments = CommentField()
 
 
     model = ContactGroup
     model = ContactGroup
     fieldsets = (
     fieldsets = (
         FieldSet('parent', 'description'),
         FieldSet('parent', 'description'),
     )
     )
-    nullable_fields = ('parent', 'description')
+    nullable_fields = ('parent', 'description', 'comments')
 
 
 
 
 class ContactRoleBulkEditForm(NetBoxModelBulkEditForm):
 class ContactRoleBulkEditForm(NetBoxModelBulkEditForm):

+ 2 - 2
netbox/tenancy/forms/bulk_import.py

@@ -31,7 +31,7 @@ class TenantGroupImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         model = TenantGroup
         model = TenantGroup
-        fields = ('name', 'slug', 'parent', 'description', 'tags')
+        fields = ('name', 'slug', 'parent', 'description', 'tags', 'comments')
 
 
 
 
 class TenantImportForm(NetBoxModelImportForm):
 class TenantImportForm(NetBoxModelImportForm):
@@ -65,7 +65,7 @@ class ContactGroupImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         model = ContactGroup
         model = ContactGroup
-        fields = ('name', 'slug', 'parent', 'description', 'tags')
+        fields = ('name', 'slug', 'parent', 'description', 'tags', 'comments')
 
 
 
 
 class ContactRoleImportForm(NetBoxModelImportForm):
 class ContactRoleImportForm(NetBoxModelImportForm):

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

@@ -27,6 +27,7 @@ class TenantGroupForm(NetBoxModelForm):
         required=False
         required=False
     )
     )
     slug = SlugField()
     slug = SlugField()
+    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet('parent', 'name', 'slug', 'description', 'tags', name=_('Tenant Group')),
         FieldSet('parent', 'name', 'slug', 'description', 'tags', name=_('Tenant Group')),
@@ -35,7 +36,7 @@ class TenantGroupForm(NetBoxModelForm):
     class Meta:
     class Meta:
         model = TenantGroup
         model = TenantGroup
         fields = [
         fields = [
-            'parent', 'name', 'slug', 'description', 'tags',
+            'parent', 'name', 'slug', 'description', 'tags', 'comments'
         ]
         ]
 
 
 
 
@@ -70,6 +71,7 @@ class ContactGroupForm(NetBoxModelForm):
         required=False
         required=False
     )
     )
     slug = SlugField()
     slug = SlugField()
+    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet('parent', 'name', 'slug', 'description', 'tags', name=_('Contact Group')),
         FieldSet('parent', 'name', 'slug', 'description', 'tags', name=_('Contact Group')),
@@ -77,7 +79,7 @@ class ContactGroupForm(NetBoxModelForm):
 
 
     class Meta:
     class Meta:
         model = ContactGroup
         model = ContactGroup
-        fields = ('parent', 'name', 'slug', 'description', 'tags')
+        fields = ('parent', 'name', 'slug', 'description', 'tags', 'comments')
 
 
 
 
 class ContactRoleForm(NetBoxModelForm):
 class ContactRoleForm(NetBoxModelForm):

+ 21 - 0
netbox/tenancy/migrations/0018_contactgroup_comments_tenantgroup_comments.py

@@ -0,0 +1,21 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('tenancy', '0017_natural_ordering'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='contactgroup',
+            name='comments',
+            field=models.TextField(blank=True),
+        ),
+        migrations.AddField(
+            model_name='tenantgroup',
+            name='comments',
+            field=models.TextField(blank=True),
+        ),
+    ]

+ 2 - 0
netbox/tenancy/search.py

@@ -25,6 +25,7 @@ class ContactGroupIndex(SearchIndex):
         ('name', 100),
         ('name', 100),
         ('slug', 110),
         ('slug', 110),
         ('description', 500),
         ('description', 500),
+        ('comments', 5000),
     )
     )
     display_attrs = ('description',)
     display_attrs = ('description',)
 
 
@@ -59,5 +60,6 @@ class TenantGroupIndex(SearchIndex):
         ('name', 100),
         ('name', 100),
         ('slug', 110),
         ('slug', 110),
         ('description', 500),
         ('description', 500),
+        ('comments', 5000),
     )
     )
     display_attrs = ('description',)
     display_attrs = ('description',)

+ 5 - 1
netbox/tenancy/tables/contacts.py

@@ -27,11 +27,15 @@ class ContactGroupTable(NetBoxTable):
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='tenancy:contactgroup_list'
         url_name='tenancy:contactgroup_list'
     )
     )
+    comments = columns.MarkdownColumn(
+        verbose_name=_('Comments'),
+    )
 
 
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = ContactGroup
         model = ContactGroup
         fields = (
         fields = (
-            'pk', 'name', 'contact_count', 'description', 'slug', 'tags', 'created', 'last_updated', 'actions',
+            'pk', 'name', 'contact_count', 'description', 'comments', 'slug', 'tags', 'created',
+            'last_updated', 'actions',
         )
         )
         default_columns = ('pk', 'name', 'contact_count', 'description')
         default_columns = ('pk', 'name', 'contact_count', 'description')
 
 

+ 5 - 1
netbox/tenancy/tables/tenants.py

@@ -24,11 +24,15 @@ class TenantGroupTable(NetBoxTable):
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='tenancy:tenantgroup_list'
         url_name='tenancy:tenantgroup_list'
     )
     )
+    comments = columns.MarkdownColumn(
+        verbose_name=_('Comments'),
+    )
 
 
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = TenantGroup
         model = TenantGroup
         fields = (
         fields = (
-            'pk', 'id', 'name', 'tenant_count', 'description', 'slug', 'tags', 'created', 'last_updated', 'actions',
+            'pk', 'id', 'name', 'tenant_count', 'description', 'comments', 'slug', 'tags', 'created',
+            'last_updated', 'actions',
         )
         )
         default_columns = ('pk', 'name', 'tenant_count', 'description')
         default_columns = ('pk', 'name', 'tenant_count', 'description')
 
 

+ 18 - 4
netbox/tenancy/tests/test_api.py

@@ -21,6 +21,7 @@ class TenantGroupTest(APIViewTestCases.APIViewTestCase):
     brief_fields = ['_depth', 'description', 'display', 'id', 'name', 'slug', 'tenant_count', 'url']
     brief_fields = ['_depth', 'description', 'display', 'id', 'name', 'slug', 'tenant_count', 'url']
     bulk_update_data = {
     bulk_update_data = {
         'description': 'New description',
         'description': 'New description',
+        'comments': 'New Comment',
     }
     }
 
 
     @classmethod
     @classmethod
@@ -28,12 +29,17 @@ class TenantGroupTest(APIViewTestCases.APIViewTestCase):
 
 
         parent_tenant_groups = (
         parent_tenant_groups = (
             TenantGroup.objects.create(name='Parent Tenant Group 1', slug='parent-tenant-group-1'),
             TenantGroup.objects.create(name='Parent Tenant Group 1', slug='parent-tenant-group-1'),
-            TenantGroup.objects.create(name='Parent Tenant Group 2', slug='parent-tenant-group-2'),
+            TenantGroup.objects.create(
+                name='Parent Tenant Group 2', slug='parent-tenant-group-2', comments='Parent Group 2 comment',
+            ),
         )
         )
 
 
         TenantGroup.objects.create(name='Tenant Group 1', slug='tenant-group-1', parent=parent_tenant_groups[0])
         TenantGroup.objects.create(name='Tenant Group 1', slug='tenant-group-1', parent=parent_tenant_groups[0])
         TenantGroup.objects.create(name='Tenant Group 2', slug='tenant-group-2', parent=parent_tenant_groups[0])
         TenantGroup.objects.create(name='Tenant Group 2', slug='tenant-group-2', parent=parent_tenant_groups[0])
-        TenantGroup.objects.create(name='Tenant Group 3', slug='tenant-group-3', parent=parent_tenant_groups[0])
+        TenantGroup.objects.create(
+            name='Tenant Group 3', slug='tenant-group-3', parent=parent_tenant_groups[0],
+            comments='Tenant Group 3 comment'
+        )
 
 
         cls.create_data = [
         cls.create_data = [
             {
             {
@@ -50,6 +56,7 @@ class TenantGroupTest(APIViewTestCases.APIViewTestCase):
                 'name': 'Tenant Group 6',
                 'name': 'Tenant Group 6',
                 'slug': 'tenant-group-6',
                 'slug': 'tenant-group-6',
                 'parent': parent_tenant_groups[1].pk,
                 'parent': parent_tenant_groups[1].pk,
+                'comments': 'Tenant Group 6 comment',
             },
             },
         ]
         ]
 
 
@@ -107,13 +114,18 @@ class ContactGroupTest(APIViewTestCases.APIViewTestCase):
     def setUpTestData(cls):
     def setUpTestData(cls):
 
 
         parent_contact_groups = (
         parent_contact_groups = (
-            ContactGroup.objects.create(name='Parent Contact Group 1', slug='parent-contact-group-1'),
+            ContactGroup.objects.create(
+                name='Parent Contact Group 1', slug='parent-contact-group-1', comments='Parent 1 comment'
+            ),
             ContactGroup.objects.create(name='Parent Contact Group 2', slug='parent-contact-group-2'),
             ContactGroup.objects.create(name='Parent Contact Group 2', slug='parent-contact-group-2'),
         )
         )
 
 
         ContactGroup.objects.create(name='Contact Group 1', slug='contact-group-1', parent=parent_contact_groups[0])
         ContactGroup.objects.create(name='Contact Group 1', slug='contact-group-1', parent=parent_contact_groups[0])
         ContactGroup.objects.create(name='Contact Group 2', slug='contact-group-2', parent=parent_contact_groups[0])
         ContactGroup.objects.create(name='Contact Group 2', slug='contact-group-2', parent=parent_contact_groups[0])
-        ContactGroup.objects.create(name='Contact Group 3', slug='contact-group-3', parent=parent_contact_groups[0])
+        ContactGroup.objects.create(
+            name='Contact Group 3', slug='contact-group-3', parent=parent_contact_groups[0],
+            comments='Child Group 3 comment',
+        )
 
 
         cls.create_data = [
         cls.create_data = [
             {
             {
@@ -125,11 +137,13 @@ class ContactGroupTest(APIViewTestCases.APIViewTestCase):
                 'name': 'Contact Group 5',
                 'name': 'Contact Group 5',
                 'slug': 'contact-group-5',
                 'slug': 'contact-group-5',
                 'parent': parent_contact_groups[1].pk,
                 'parent': parent_contact_groups[1].pk,
+                'comments': '',
             },
             },
             {
             {
                 'name': 'Contact Group 6',
                 'name': 'Contact Group 6',
                 'slug': 'contact-group-6',
                 'slug': 'contact-group-6',
                 'parent': parent_contact_groups[1].pk,
                 'parent': parent_contact_groups[1].pk,
+                'comments': 'Child Group 6 comment',
             },
             },
         ]
         ]
 
 

+ 28 - 6
netbox/tenancy/tests/test_filtersets.py

@@ -16,7 +16,7 @@ class TenantGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
 
 
         parent_tenant_groups = (
         parent_tenant_groups = (
             TenantGroup(name='Tenant Group 1', slug='tenant-group-1'),
             TenantGroup(name='Tenant Group 1', slug='tenant-group-1'),
-            TenantGroup(name='Tenant Group 2', slug='tenant-group-2'),
+            TenantGroup(name='Tenant Group 2', slug='tenant-group-2', comments='Parent group 2 comment'),
             TenantGroup(name='Tenant Group 3', slug='tenant-group-3'),
             TenantGroup(name='Tenant Group 3', slug='tenant-group-3'),
         )
         )
         for tenant_group in parent_tenant_groups:
         for tenant_group in parent_tenant_groups:
@@ -27,7 +27,8 @@ class TenantGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
                 name='Tenant Group 1A',
                 name='Tenant Group 1A',
                 slug='tenant-group-1a',
                 slug='tenant-group-1a',
                 parent=parent_tenant_groups[0],
                 parent=parent_tenant_groups[0],
-                description='foobar1'
+                description='foobar1',
+                comments='Tenant Group 1A comment',
             ),
             ),
             TenantGroup(
             TenantGroup(
                 name='Tenant Group 2A',
                 name='Tenant Group 2A',
@@ -48,7 +49,10 @@ class TenantGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
         child_tenant_groups = (
         child_tenant_groups = (
             TenantGroup(name='Tenant Group 1A1', slug='tenant-group-1a1', parent=tenant_groups[0]),
             TenantGroup(name='Tenant Group 1A1', slug='tenant-group-1a1', parent=tenant_groups[0]),
             TenantGroup(name='Tenant Group 2A1', slug='tenant-group-2a1', parent=tenant_groups[1]),
             TenantGroup(name='Tenant Group 2A1', slug='tenant-group-2a1', parent=tenant_groups[1]),
-            TenantGroup(name='Tenant Group 3A1', slug='tenant-group-3a1', parent=tenant_groups[2]),
+            TenantGroup(
+                name='Tenant Group 3A1', slug='tenant-group-3a1', parent=tenant_groups[2],
+                comments='Tenant Group 3A1 comment',
+            ),
         )
         )
         for tenant_group in child_tenant_groups:
         for tenant_group in child_tenant_groups:
             tenant_group.save()
             tenant_group.save()
@@ -57,6 +61,13 @@ class TenantGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'q': 'foobar1'}
         params = {'q': 'foobar1'}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
+    def test_q_comments(self):
+        params = {'q': 'parent'}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+        params = {'q': 'comment'}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+
     def test_name(self):
     def test_name(self):
         params = {'name': ['Tenant Group 1', 'Tenant Group 2']}
         params = {'name': ['Tenant Group 1', 'Tenant Group 2']}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -139,7 +150,7 @@ class ContactGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
 
 
         parent_contact_groups = (
         parent_contact_groups = (
             ContactGroup(name='Contact Group 1', slug='contact-group-1'),
             ContactGroup(name='Contact Group 1', slug='contact-group-1'),
-            ContactGroup(name='Contact Group 2', slug='contact-group-2'),
+            ContactGroup(name='Contact Group 2', slug='contact-group-2', comments='Parent group 2'),
             ContactGroup(name='Contact Group 3', slug='contact-group-3'),
             ContactGroup(name='Contact Group 3', slug='contact-group-3'),
         )
         )
         for contact_group in parent_contact_groups:
         for contact_group in parent_contact_groups:
@@ -162,14 +173,18 @@ class ContactGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
                 name='Contact Group 3A',
                 name='Contact Group 3A',
                 slug='contact-group-3a',
                 slug='contact-group-3a',
                 parent=parent_contact_groups[2],
                 parent=parent_contact_groups[2],
-                description='foobar3'
+                description='foobar3',
+                comments='Contact Group 3A comment, not a parent',
             ),
             ),
         )
         )
         for contact_group in contact_groups:
         for contact_group in contact_groups:
             contact_group.save()
             contact_group.save()
 
 
         child_contact_groups = (
         child_contact_groups = (
-            ContactGroup(name='Contact Group 1A1', slug='contact-group-1a1', parent=contact_groups[0]),
+            ContactGroup(
+                name='Contact Group 1A1', slug='contact-group-1a1', parent=contact_groups[0],
+                comments='Contact Group 1A1 comment',
+            ),
             ContactGroup(name='Contact Group 2A1', slug='contact-group-2a1', parent=contact_groups[1]),
             ContactGroup(name='Contact Group 2A1', slug='contact-group-2a1', parent=contact_groups[1]),
             ContactGroup(name='Contact Group 3A1', slug='contact-group-3a1', parent=contact_groups[2]),
             ContactGroup(name='Contact Group 3A1', slug='contact-group-3a1', parent=contact_groups[2]),
         )
         )
@@ -180,6 +195,13 @@ class ContactGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'q': 'foobar1'}
         params = {'q': 'foobar1'}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
+    def test_q_comments(self):
+        params = {'q': 'parent'}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+        params = {'q': '1A1'}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
     def test_name(self):
     def test_name(self):
         params = {'name': ['Contact Group 1', 'Contact Group 2']}
         params = {'name': ['Contact Group 1', 'Contact Group 2']}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

+ 22 - 18
netbox/tenancy/tests/test_views.py

@@ -15,7 +15,7 @@ class TenantGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
 
 
         tenant_groups = (
         tenant_groups = (
             TenantGroup(name='Tenant Group 1', slug='tenant-group-1'),
             TenantGroup(name='Tenant Group 1', slug='tenant-group-1'),
-            TenantGroup(name='Tenant Group 2', slug='tenant-group-2'),
+            TenantGroup(name='Tenant Group 2', slug='tenant-group-2', comments='Tenant Group 2 comment'),
             TenantGroup(name='Tenant Group 3', slug='tenant-group-3'),
             TenantGroup(name='Tenant Group 3', slug='tenant-group-3'),
         )
         )
         for tenanantgroup in tenant_groups:
         for tenanantgroup in tenant_groups:
@@ -28,24 +28,26 @@ class TenantGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
             'slug': 'tenant-group-x',
             'slug': 'tenant-group-x',
             'description': 'A new tenant group',
             'description': 'A new tenant group',
             'tags': [t.pk for t in tags],
             'tags': [t.pk for t in tags],
+            'comments': 'Tenant Group X comment',
         }
         }
 
 
         cls.csv_data = (
         cls.csv_data = (
-            "name,slug,description",
-            "Tenant Group 4,tenant-group-4,Fourth tenant group",
-            "Tenant Group 5,tenant-group-5,Fifth tenant group",
-            "Tenant Group 6,tenant-group-6,Sixth tenant group",
+            "name,slug,description,comments",
+            "Tenant Group 4,tenant-group-4,Fourth tenant group,",
+            "Tenant Group 5,tenant-group-5,Fifth tenant group,",
+            "Tenant Group 6,tenant-group-6,Sixth tenant group,Sixth tenant group comment",
         )
         )
 
 
         cls.csv_update_data = (
         cls.csv_update_data = (
-            "id,name,description",
-            f"{tenant_groups[0].pk},Tenant Group 7,Fourth tenant group7",
-            f"{tenant_groups[1].pk},Tenant Group 8,Fifth tenant group8",
-            f"{tenant_groups[2].pk},Tenant Group 0,Sixth tenant group9",
+            "id,name,description,comments",
+            f"{tenant_groups[0].pk},Tenant Group 7,Fourth tenant group7,Group 7 comment",
+            f"{tenant_groups[1].pk},Tenant Group 8,Fifth tenant group8,",
+            f"{tenant_groups[2].pk},Tenant Group 0,Sixth tenant group9,",
         )
         )
 
 
         cls.bulk_edit_data = {
         cls.bulk_edit_data = {
             'description': 'New description',
             'description': 'New description',
+            'comments': 'New comment',
         }
         }
 
 
 
 
@@ -106,7 +108,7 @@ class ContactGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
     def setUpTestData(cls):
     def setUpTestData(cls):
 
 
         contact_groups = (
         contact_groups = (
-            ContactGroup(name='Contact Group 1', slug='contact-group-1'),
+            ContactGroup(name='Contact Group 1', slug='contact-group-1', comments='Comment 1'),
             ContactGroup(name='Contact Group 2', slug='contact-group-2'),
             ContactGroup(name='Contact Group 2', slug='contact-group-2'),
             ContactGroup(name='Contact Group 3', slug='contact-group-3'),
             ContactGroup(name='Contact Group 3', slug='contact-group-3'),
         )
         )
@@ -120,24 +122,26 @@ class ContactGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
             'slug': 'contact-group-x',
             'slug': 'contact-group-x',
             'description': 'A new contact group',
             'description': 'A new contact group',
             'tags': [t.pk for t in tags],
             'tags': [t.pk for t in tags],
+            'comments': 'Form data comment',
         }
         }
 
 
         cls.csv_data = (
         cls.csv_data = (
-            "name,slug,description",
-            "Contact Group 4,contact-group-4,Fourth contact group",
-            "Contact Group 5,contact-group-5,Fifth contact group",
-            "Contact Group 6,contact-group-6,Sixth contact group",
+            "name,slug,description,comments",
+            "Contact Group 4,contact-group-4,Fourth contact group,",
+            "Contact Group 5,contact-group-5,Fifth contact group,Fifth comment",
+            "Contact Group 6,contact-group-6,Sixth contact group,",
         )
         )
 
 
         cls.csv_update_data = (
         cls.csv_update_data = (
-            "id,name,description",
-            f"{contact_groups[0].pk},Contact Group 7,Fourth contact group7",
-            f"{contact_groups[1].pk},Contact Group 8,Fifth contact group8",
-            f"{contact_groups[2].pk},Contact Group 0,Sixth contact group9",
+            "id,name,description,comments",
+            f"{contact_groups[0].pk},Contact Group 7,Fourth contact group7,",
+            f"{contact_groups[1].pk},Contact Group 8,Fifth contact group8,Group 8 comment",
+            f"{contact_groups[2].pk},Contact Group 0,Sixth contact group9,",
         )
         )
 
 
         cls.bulk_edit_data = {
         cls.bulk_edit_data = {
             'description': 'New description',
             'description': 'New description',
+            'comments': 'Bulk update comment',
         }
         }
 
 
 
 

+ 1 - 1
netbox/wireless/api/serializers_/wirelesslans.py

@@ -26,7 +26,7 @@ class WirelessLANGroupSerializer(NestedGroupModelSerializer):
         model = WirelessLANGroup
         model = WirelessLANGroup
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields',
             'id', 'url', 'display_url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields',
-            'created', 'last_updated', 'wirelesslan_count', '_depth',
+            'created', 'last_updated', 'wirelesslan_count', 'comments', '_depth',
         ]
         ]
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'wirelesslan_count', '_depth')
         brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'wirelesslan_count', '_depth')
 
 

+ 2 - 2
netbox/wireless/filtersets.py

@@ -5,7 +5,7 @@ from dcim.choices import LinkStatusChoices
 from dcim.base_filtersets import ScopedFilterSet
 from dcim.base_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 NestedGroupModelFilterSet, NetBoxModelFilterSet
 from tenancy.filtersets import TenancyFilterSet
 from tenancy.filtersets import TenancyFilterSet
 from utilities.filters import TreeNodeMultipleChoiceFilter
 from utilities.filters import TreeNodeMultipleChoiceFilter
 from .choices import *
 from .choices import *
@@ -18,7 +18,7 @@ __all__ = (
 )
 )
 
 
 
 
-class WirelessLANGroupFilterSet(OrganizationalModelFilterSet):
+class WirelessLANGroupFilterSet(NestedGroupModelFilterSet):
     parent_id = django_filters.ModelMultipleChoiceFilter(
     parent_id = django_filters.ModelMultipleChoiceFilter(
         queryset=WirelessLANGroup.objects.all()
         queryset=WirelessLANGroup.objects.all()
     )
     )

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

@@ -32,12 +32,13 @@ class WirelessLANGroupBulkEditForm(NetBoxModelBulkEditForm):
         max_length=200,
         max_length=200,
         required=False
         required=False
     )
     )
+    comments = CommentField()
 
 
     model = WirelessLANGroup
     model = WirelessLANGroup
     fieldsets = (
     fieldsets = (
         FieldSet('parent', 'description'),
         FieldSet('parent', 'description'),
     )
     )
-    nullable_fields = ('parent', 'description')
+    nullable_fields = ('parent', 'description', 'comments')
 
 
 
 
 class WirelessLANBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm):
 class WirelessLANBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm):

+ 1 - 1
netbox/wireless/forms/bulk_import.py

@@ -30,7 +30,7 @@ class WirelessLANGroupImportForm(NetBoxModelImportForm):
 
 
     class Meta:
     class Meta:
         model = WirelessLANGroup
         model = WirelessLANGroup
-        fields = ('name', 'slug', 'parent', 'description', 'tags')
+        fields = ('name', 'slug', 'parent', 'description', 'tags', 'comments')
 
 
 
 
 class WirelessLANImportForm(ScopedImportForm, NetBoxModelImportForm):
 class WirelessLANImportForm(ScopedImportForm, NetBoxModelImportForm):

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

@@ -24,6 +24,7 @@ class WirelessLANGroupForm(NetBoxModelForm):
         required=False
         required=False
     )
     )
     slug = SlugField()
     slug = SlugField()
+    comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
         FieldSet('parent', 'name', 'slug', 'description', 'tags', name=_('Wireless LAN Group')),
         FieldSet('parent', 'name', 'slug', 'description', 'tags', name=_('Wireless LAN Group')),
@@ -32,7 +33,7 @@ class WirelessLANGroupForm(NetBoxModelForm):
     class Meta:
     class Meta:
         model = WirelessLANGroup
         model = WirelessLANGroup
         fields = [
         fields = [
-            'parent', 'name', 'slug', 'description', 'tags',
+            'parent', 'name', 'slug', 'description', 'tags', 'comments',
         ]
         ]
 
 
 
 

+ 16 - 0
netbox/wireless/migrations/0014_wirelesslangroup_comments.py

@@ -0,0 +1,16 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('wireless', '0013_natural_ordering'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='wirelesslangroup',
+            name='comments',
+            field=models.TextField(blank=True),
+        ),
+    ]

+ 1 - 0
netbox/wireless/search.py

@@ -21,6 +21,7 @@ class WirelessLANGroupIndex(SearchIndex):
         ('name', 100),
         ('name', 100),
         ('slug', 110),
         ('slug', 110),
         ('description', 500),
         ('description', 500),
+        ('comments', 5000),
     )
     )
     display_attrs = ('description',)
     display_attrs = ('description',)
 
 

+ 3 - 0
netbox/wireless/tests/test_api.py

@@ -24,10 +24,12 @@ class WirelessLANGroupTest(APIViewTestCases.APIViewTestCase):
         {
         {
             'name': 'Wireless LAN Group 4',
             'name': 'Wireless LAN Group 4',
             'slug': 'wireless-lan-group-4',
             'slug': 'wireless-lan-group-4',
+            'comments': '',
         },
         },
         {
         {
             'name': 'Wireless LAN Group 5',
             'name': 'Wireless LAN Group 5',
             'slug': 'wireless-lan-group-5',
             'slug': 'wireless-lan-group-5',
+            'comments': 'LAN Group 5 comment',
         },
         },
         {
         {
             'name': 'Wireless LAN Group 6',
             'name': 'Wireless LAN Group 6',
@@ -36,6 +38,7 @@ class WirelessLANGroupTest(APIViewTestCases.APIViewTestCase):
     ]
     ]
     bulk_update_data = {
     bulk_update_data = {
         'description': 'New description',
         'description': 'New description',
+        'comments': 'New comment',
     }
     }
 
 
     @classmethod
     @classmethod

+ 17 - 2
netbox/wireless/tests/test_filtersets.py

@@ -21,7 +21,10 @@ class WirelessLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
         parent_groups = (
         parent_groups = (
             WirelessLANGroup(name='Wireless LAN Group 1', slug='wireless-lan-group-1', description='A'),
             WirelessLANGroup(name='Wireless LAN Group 1', slug='wireless-lan-group-1', description='A'),
             WirelessLANGroup(name='Wireless LAN Group 2', slug='wireless-lan-group-2', description='B'),
             WirelessLANGroup(name='Wireless LAN Group 2', slug='wireless-lan-group-2', description='B'),
-            WirelessLANGroup(name='Wireless LAN Group 3', slug='wireless-lan-group-3', description='C'),
+            WirelessLANGroup(
+                name='Wireless LAN Group 3', slug='wireless-lan-group-3', description='C',
+                comments='Parent Group 3 comment',
+            ),
         )
         )
         for group in parent_groups:
         for group in parent_groups:
             group.save()
             group.save()
@@ -38,10 +41,15 @@ class WirelessLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
                 slug='wireless-lan-group-1b',
                 slug='wireless-lan-group-1b',
                 parent=parent_groups[0],
                 parent=parent_groups[0],
                 description='foobar2',
                 description='foobar2',
+                comments='Child Group 1B comment',
             ),
             ),
             WirelessLANGroup(name='Wireless LAN Group 2A', slug='wireless-lan-group-2a', parent=parent_groups[1]),
             WirelessLANGroup(name='Wireless LAN Group 2A', slug='wireless-lan-group-2a', parent=parent_groups[1]),
             WirelessLANGroup(name='Wireless LAN Group 2B', slug='wireless-lan-group-2b', parent=parent_groups[1]),
             WirelessLANGroup(name='Wireless LAN Group 2B', slug='wireless-lan-group-2b', parent=parent_groups[1]),
-            WirelessLANGroup(name='Wireless LAN Group 3A', slug='wireless-lan-group-3a', parent=parent_groups[2]),
+            WirelessLANGroup(
+                name='Wireless LAN Group 3A', slug='wireless-lan-group-3a', parent=parent_groups[2],
+                comments='Wireless LAN Group 3A comment',
+
+            ),
             WirelessLANGroup(name='Wireless LAN Group 3B', slug='wireless-lan-group-3b', parent=parent_groups[2]),
             WirelessLANGroup(name='Wireless LAN Group 3B', slug='wireless-lan-group-3b', parent=parent_groups[2]),
         )
         )
         for group in groups:
         for group in groups:
@@ -62,6 +70,13 @@ class WirelessLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'q': 'foobar1'}
         params = {'q': 'foobar1'}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
+    def test_q_comments(self):
+        params = {'q': 'parent'}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+        params = {'q': 'comment'}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+
     def test_name(self):
     def test_name(self):
         params = {'name': ['Wireless LAN Group 1', 'Wireless LAN Group 2']}
         params = {'name': ['Wireless LAN Group 1', 'Wireless LAN Group 2']}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

+ 13 - 9
netbox/wireless/tests/test_views.py

@@ -16,7 +16,9 @@ class WirelessLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
 
 
         groups = (
         groups = (
             WirelessLANGroup(name='Wireless LAN Group 1', slug='wireless-lan-group-1'),
             WirelessLANGroup(name='Wireless LAN Group 1', slug='wireless-lan-group-1'),
-            WirelessLANGroup(name='Wireless LAN Group 2', slug='wireless-lan-group-2'),
+            WirelessLANGroup(
+                name='Wireless LAN Group 2', slug='wireless-lan-group-2', comments='LAN Group 2 comment',
+            ),
             WirelessLANGroup(name='Wireless LAN Group 3', slug='wireless-lan-group-3'),
             WirelessLANGroup(name='Wireless LAN Group 3', slug='wireless-lan-group-3'),
         )
         )
         for group in groups:
         for group in groups:
@@ -30,24 +32,26 @@ class WirelessLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
             'parent': groups[2].pk,
             'parent': groups[2].pk,
             'description': 'A new wireless LAN group',
             'description': 'A new wireless LAN group',
             'tags': [t.pk for t in tags],
             'tags': [t.pk for t in tags],
+            'comments': 'LAN Group X comment',
         }
         }
 
 
         cls.csv_data = (
         cls.csv_data = (
-            "name,slug,description",
-            "Wireless LAN Group 4,wireless-lan-group-4,Fourth wireless LAN group",
-            "Wireless LAN Group 5,wireless-lan-group-5,Fifth wireless LAN group",
-            "Wireless LAN Group 6,wireless-lan-group-6,Sixth wireless LAN group",
+            "name,slug,description,comments",
+            "Wireless LAN Group 4,wireless-lan-group-4,Fourth wireless LAN group,",
+            "Wireless LAN Group 5,wireless-lan-group-5,Fifth wireless LAN group,",
+            "Wireless LAN Group 6,wireless-lan-group-6,Sixth wireless LAN group,LAN Group 6 comment",
         )
         )
 
 
         cls.csv_update_data = (
         cls.csv_update_data = (
-            "id,name,description",
-            f"{groups[0].pk},Wireless LAN Group 7,Fourth wireless LAN group7",
-            f"{groups[1].pk},Wireless LAN Group 8,Fifth wireless LAN group8",
-            f"{groups[2].pk},Wireless LAN Group 0,Sixth wireless LAN group9",
+            "id,name,description,comments",
+            f"{groups[0].pk},Wireless LAN Group 7,Fourth wireless LAN group7,Group 7 comment",
+            f"{groups[1].pk},Wireless LAN Group 8,Fifth wireless LAN group8,",
+            f"{groups[2].pk},Wireless LAN Group 0,Sixth wireless LAN group9,",
         )
         )
 
 
         cls.bulk_edit_data = {
         cls.bulk_edit_data = {
             'description': 'New description',
             'description': 'New description',
+            'comments': 'New Comments',
         }
         }