From ae7a47ca60a275ff8574da2f2b50cc055c1091e9 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Mon, 10 Mar 2025 11:52:13 -0500 Subject: [PATCH 01/12] Adds comments field to abstract NestedGroupModel and associated migrations Models affected: - dcim: `Location`, `Region`, `SiteGroup` - tenancy`: `ContactGroup`, `TenantGroup` - wireless: `WirelessLANGroup` --- ...ents_region_comments_sitegroup_comments.py | 28 +++++++++++++++++++ netbox/netbox/models/__init__.py | 4 +++ ...tactgroup_comments_tenantgroup_comments.py | 23 +++++++++++++++ .../0014_wirelesslangroup_comments.py | 18 ++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 netbox/dcim/migrations/0202_location_comments_region_comments_sitegroup_comments.py create mode 100644 netbox/tenancy/migrations/0018_contactgroup_comments_tenantgroup_comments.py create mode 100644 netbox/wireless/migrations/0014_wirelesslangroup_comments.py diff --git a/netbox/dcim/migrations/0202_location_comments_region_comments_sitegroup_comments.py b/netbox/dcim/migrations/0202_location_comments_region_comments_sitegroup_comments.py new file mode 100644 index 000000000..51031de53 --- /dev/null +++ b/netbox/dcim/migrations/0202_location_comments_region_comments_sitegroup_comments.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.7 on 2025-03-10 16:37 + +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), + ), + ] diff --git a/netbox/netbox/models/__init__.py b/netbox/netbox/models/__init__.py index b1f7cfd48..3ad0ac556 100644 --- a/netbox/netbox/models/__init__.py +++ b/netbox/netbox/models/__init__.py @@ -150,6 +150,10 @@ class NestedGroupModel(NetBoxFeatureSet, MPTTModel): max_length=200, blank=True ) + comments = models.TextField( + verbose_name=_('comments'), + blank=True + ) objects = TreeManager() diff --git a/netbox/tenancy/migrations/0018_contactgroup_comments_tenantgroup_comments.py b/netbox/tenancy/migrations/0018_contactgroup_comments_tenantgroup_comments.py new file mode 100644 index 000000000..3481baeec --- /dev/null +++ b/netbox/tenancy/migrations/0018_contactgroup_comments_tenantgroup_comments.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.7 on 2025-03-10 16:37 + +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), + ), + ] diff --git a/netbox/wireless/migrations/0014_wirelesslangroup_comments.py b/netbox/wireless/migrations/0014_wirelesslangroup_comments.py new file mode 100644 index 000000000..3e3cab270 --- /dev/null +++ b/netbox/wireless/migrations/0014_wirelesslangroup_comments.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.7 on 2025-03-10 16:37 + +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), + ), + ] From 44efd5e833212e3e12c3f7ab75fc2d1cf3a4b314 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Mon, 10 Mar 2025 14:53:23 -0500 Subject: [PATCH 02/12] Adds Location.comments field in the required locations - [x] 1. Add the field to the model class - [x] 2. Generate and run database migrations - [NA] 3. Add validation logic to clean() - [NA] 4. Update relevant querysets - [x] 5. Update API serializer - [x] 6. Add fields to forms - [x] dcim.forms.model_forms.LocationForm, create/edit (e.g. model_forms.py) - [x] dcim.forms.buld_edit.LocationBulkEditForm, bulk edit - [x] dcim.dorms.bulk_import.LocationImportForm, CSV import - [x] filter (UI and API) - [NA] UI - Note: could not find any comments related things in filtersets - [x] API - [x] 7. Extend object filter set - [x] 8. Add column to object table - [x] 9. Update the SearchIndex - [x] 10. Update the UI templates - [x] 11. Create/extend test cases - [NA] models - [x] views - [NA] forms - [x] filtersets - [x] api - [NA] 12. Update the model's documentation --- netbox/dcim/api/serializers_/sites.py | 2 +- netbox/dcim/filtersets.py | 3 ++- netbox/dcim/forms/bulk_edit.py | 3 ++- netbox/dcim/forms/bulk_import.py | 5 ++++- netbox/dcim/forms/model_forms.py | 4 +++- netbox/dcim/search.py | 1 + netbox/dcim/tables/sites.py | 2 +- netbox/dcim/tests/test_api.py | 5 +++++ netbox/dcim/tests/test_filtersets.py | 10 ++++++++++ netbox/dcim/tests/test_views.py | 21 +++++++++++++-------- 10 files changed, 42 insertions(+), 14 deletions(-) diff --git a/netbox/dcim/api/serializers_/sites.py b/netbox/dcim/api/serializers_/sites.py index b818cd954..1b95af70e 100644 --- a/netbox/dcim/api/serializers_/sites.py +++ b/netbox/dcim/api/serializers_/sites.py @@ -93,6 +93,6 @@ class LocationSerializer(NestedGroupModelSerializer): fields = [ 'id', 'url', 'display_url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'facility', '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') diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index e46730da8..bd7713289 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -280,7 +280,8 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM return queryset.filter( Q(name__icontains=value) | Q(facility__icontains=value) | - Q(description__icontains=value) + Q(description__icontains=value) | + Q(comments__icontains=value) ) diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 3b9a183cd..696474617 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -197,12 +197,13 @@ class LocationBulkEditForm(NetBoxModelBulkEditForm): max_length=200, required=False ) + comments = CommentField() model = Location fieldsets = ( FieldSet('site', 'parent', 'status', 'tenant', 'description'), ) - nullable_fields = ('parent', 'tenant', 'description') + nullable_fields = ('parent', 'tenant', 'description', 'comments') class RackRoleBulkEditForm(NetBoxModelBulkEditForm): diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 92f7220da..31a6d93a4 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -160,7 +160,10 @@ class LocationImportForm(NetBoxModelImportForm): class Meta: 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): super().__init__(data, *args, **kwargs) diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 91e23e8b1..e1535fe0c 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -179,6 +179,7 @@ class LocationForm(TenancyForm, NetBoxModelForm): } ) slug = SlugField() + comments = CommentField() fieldsets = ( FieldSet('site', 'parent', 'name', 'slug', 'status', 'facility', 'description', 'tags', name=_('Location')), @@ -188,7 +189,8 @@ class LocationForm(TenancyForm, NetBoxModelForm): class Meta: model = Location fields = ( - 'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant', 'facility', 'tags', + 'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant', + 'facility', 'tags', 'comments', ) diff --git a/netbox/dcim/search.py b/netbox/dcim/search.py index 5dea2a09b..b7299c111 100644 --- a/netbox/dcim/search.py +++ b/netbox/dcim/search.py @@ -144,6 +144,7 @@ class LocationIndex(SearchIndex): ('facility', 100), ('slug', 110), ('description', 500), + ('comments', 5000), ) display_attrs = ('site', 'status', 'tenant', 'facility', 'description') diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py index 77844f086..0209a1742 100644 --- a/netbox/dcim/tables/sites.py +++ b/netbox/dcim/tables/sites.py @@ -158,7 +158,7 @@ class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): model = Location fields = ( '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 = ( 'pk', 'name', 'site', 'status', 'facility', 'tenant', 'rack_count', 'device_count', 'description' diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 08f93f6ea..1eacd7ea7 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -212,12 +212,14 @@ class LocationTest(APIViewTestCases.APIViewTestCase): name='Parent Location 1', slug='parent-location-1', status=LocationStatusChoices.STATUS_ACTIVE, + comments='First!' ), Location.objects.create( site=sites[1], name='Parent Location 2', slug='parent-location-2', status=LocationStatusChoices.STATUS_ACTIVE, + comments='Second!' ), ) @@ -227,6 +229,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase): slug='location-1', parent=parent_locations[0], status=LocationStatusChoices.STATUS_ACTIVE, + comments='Third!' ) Location.objects.create( site=sites[0], @@ -250,6 +253,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase): 'site': sites[1].pk, 'parent': parent_locations[1].pk, 'status': LocationStatusChoices.STATUS_PLANNED, + 'comments': '', }, { 'name': 'Test Location 5', @@ -257,6 +261,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase): 'site': sites[1].pk, 'parent': parent_locations[1].pk, 'status': LocationStatusChoices.STATUS_PLANNED, + 'comments': 'Somebody should check on this location', }, { 'name': 'Test Location 6', diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index 7c9b8adc6..d8526062b 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -401,6 +401,7 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests): status=LocationStatusChoices.STATUS_PLANNED, facility='Facility 1', description='foobar1', + comments='', ), Location( name='Location 2A', @@ -410,6 +411,7 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests): status=LocationStatusChoices.STATUS_STAGING, facility='Facility 2', description='foobar2', + comments='First comment!', ), Location( name='Location 3A', @@ -419,6 +421,7 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests): status=LocationStatusChoices.STATUS_DECOMMISSIONING, facility='Facility 3', description='foobar3', + comments='_This_ is a **bold comment**', ), ) for location in locations: @@ -436,6 +439,13 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'q': 'foobar1'} 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): params = {'name': ['Location 1', 'Location 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 4dea94c7d..4feffcf1f 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -202,6 +202,7 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase): site=site, status=LocationStatusChoices.STATUS_ACTIVE, tenant=tenant, + comments='', ), Location( name='Location 2', @@ -209,6 +210,7 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase): site=site, status=LocationStatusChoices.STATUS_ACTIVE, tenant=tenant, + comments='First comment!', ), Location( name='Location 3', @@ -216,6 +218,7 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase): site=site, status=LocationStatusChoices.STATUS_ACTIVE, tenant=tenant, + comments='_This_ is a **bold comment**', ), ) for location in locations: @@ -232,24 +235,26 @@ class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase): 'tenant': tenant.pk, 'description': 'A new location', 'tags': [t.pk for t in tags], + 'comments': 'This comment is really boring', } 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 = ( - "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 = { 'description': 'New description', + 'comments': 'This comment is also really boring', } From 2e2c815c9140528f0ae537b8327921e9c5d9ffe2 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Mon, 10 Mar 2025 17:05:26 -0500 Subject: [PATCH 03/12] Update Location detail UI template --- netbox/templates/dcim/location.html | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox/templates/dcim/location.html b/netbox/templates/dcim/location.html index 97dcc20f0..02e02a1ed 100644 --- a/netbox/templates/dcim/location.html +++ b/netbox/templates/dcim/location.html @@ -62,6 +62,7 @@ {% include 'inc/panels/tags.html' %} {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/comments.html' %} {% plugin_left_page object %}
From 9a9d6cdedb6f06687b7c7a350b9ae7fcfd6a4e37 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Mon, 10 Mar 2025 17:07:59 -0500 Subject: [PATCH 04/12] Adds Region.comments field in the required locations - [x] 1. Add the field to the model class - [x] 2. Generate and run database migrations - [NA] 3. Add validation logic to clean() - [NA] 4. Update relevant querysets - [x] 5. Update API serializer - [ ] 6. Add fields to forms - [x] dcim.forms.model_forms.RegionForm, create/edit (e.g. model_forms.py) - [x] dcim.forms.buld_edit.RegionBulkEditForm, bulk edit - [x] dcim.dorms.bulk_import.RegionImportForm, CSV import - [NA] filter (UI and API) - [x] 7. Extend object filter set - [x] 8. Add column to object table - [x] 9. Update the SearchIndex - [x] 10. Update the UI templates - [x] 11. Create/extend test cases - [NA] models - [x] views - [NA] forms - [x] filtersets - [x] api - [NA] 12. Update the model's documentation --- netbox/dcim/api/serializers_/sites.py | 2 +- netbox/dcim/filtersets.py | 9 +++++++++ netbox/dcim/forms/bulk_edit.py | 3 ++- netbox/dcim/forms/bulk_import.py | 2 +- netbox/dcim/forms/model_forms.py | 3 ++- netbox/dcim/search.py | 1 + netbox/dcim/tables/sites.py | 4 ++-- netbox/dcim/tests/test_api.py | 4 +++- netbox/dcim/tests/test_filtersets.py | 19 ++++++++++++++++--- netbox/dcim/tests/test_views.py | 16 ++++++++++------ netbox/templates/dcim/region.html | 1 + 11 files changed, 48 insertions(+), 16 deletions(-) diff --git a/netbox/dcim/api/serializers_/sites.py b/netbox/dcim/api/serializers_/sites.py index 1b95af70e..70924de5d 100644 --- a/netbox/dcim/api/serializers_/sites.py +++ b/netbox/dcim/api/serializers_/sites.py @@ -27,7 +27,7 @@ class RegionSerializer(NestedGroupModelSerializer): model = Region 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') diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index bd7713289..6517d277a 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -110,6 +110,15 @@ class RegionFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet): model = Region fields = ('id', 'name', 'slug', 'description') + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(name__icontains=value) | + Q(description__icontains=value) | + Q(comments__icontains=value) + ).distinct() + class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet): parent_id = django_filters.ModelMultipleChoiceFilter( diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 696474617..dd78c0b23 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -78,12 +78,13 @@ class RegionBulkEditForm(NetBoxModelBulkEditForm): max_length=200, required=False ) + comments = CommentField() model = Region fieldsets = ( FieldSet('parent', 'description'), ) - nullable_fields = ('parent', 'description') + nullable_fields = ('parent', 'description', 'comments') class SiteGroupBulkEditForm(NetBoxModelBulkEditForm): diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 31a6d93a4..cf9726360 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -68,7 +68,7 @@ class RegionImportForm(NetBoxModelImportForm): class Meta: model = Region - fields = ('name', 'slug', 'parent', 'description', 'tags') + fields = ('name', 'slug', 'parent', 'description', 'tags', 'comments') class SiteGroupImportForm(NetBoxModelImportForm): diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index e1535fe0c..639eebe13 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -78,6 +78,7 @@ class RegionForm(NetBoxModelForm): required=False ) slug = SlugField() + comments = CommentField() fieldsets = ( FieldSet('parent', 'name', 'slug', 'description', 'tags'), @@ -86,7 +87,7 @@ class RegionForm(NetBoxModelForm): class Meta: model = Region fields = ( - 'parent', 'name', 'slug', 'description', 'tags', + 'parent', 'name', 'slug', 'description', 'tags', 'comments', ) diff --git a/netbox/dcim/search.py b/netbox/dcim/search.py index b7299c111..e13c97ba7 100644 --- a/netbox/dcim/search.py +++ b/netbox/dcim/search.py @@ -318,6 +318,7 @@ class RegionIndex(SearchIndex): ('name', 100), ('slug', 110), ('description', 500), + ('comments', 5000), ) display_attrs = ('parent', 'description') diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py index 0209a1742..51e67f2f3 100644 --- a/netbox/dcim/tables/sites.py +++ b/netbox/dcim/tables/sites.py @@ -36,8 +36,8 @@ class RegionTable(ContactsColumnMixin, NetBoxTable): class Meta(NetBoxTable.Meta): model = Region 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') diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 1eacd7ea7..68cf34fe4 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -74,6 +74,7 @@ class RegionTest(APIViewTestCases.APIViewTestCase): { 'name': 'Region 4', 'slug': 'region-4', + 'comments': 'this is region 4, not region 5', }, { 'name': 'Region 5', @@ -86,13 +87,14 @@ class RegionTest(APIViewTestCases.APIViewTestCase): ] bulk_update_data = { 'description': 'New description', + 'comments': 'New comments', } @classmethod def setUpTestData(cls): 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') diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index d8526062b..ebc9f3fba 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -67,9 +67,15 @@ class RegionTestCase(TestCase, ChangeLoggedFilterSetTests): def setUpTestData(cls): 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: region.save() @@ -100,6 +106,13 @@ class RegionTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'q': 'foobar1'} 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): params = {'name': ['Region 1', 'Region 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 4feffcf1f..24fa4ae55 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -25,8 +25,10 @@ class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase): # Create three 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'), ) for region in regions: @@ -40,13 +42,14 @@ class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase): 'parent': regions[2].pk, 'description': 'A new region', 'tags': [t.pk for t in tags], + 'comments': 'This comment is really exciting!', } 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 = ( @@ -58,6 +61,7 @@ class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase): cls.bulk_edit_data = { 'description': 'New description', + 'comments': 'This comment is super exciting!!!', } diff --git a/netbox/templates/dcim/region.html b/netbox/templates/dcim/region.html index 1e1b75cd5..c6acbb9ea 100644 --- a/netbox/templates/dcim/region.html +++ b/netbox/templates/dcim/region.html @@ -41,6 +41,7 @@
{% include 'inc/panels/tags.html' %} {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/comments.html' %} {% plugin_left_page object %}
From ed98756f3eb329a2343a91eb8853b9e186ec0c8a Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Tue, 11 Mar 2025 10:04:50 -0500 Subject: [PATCH 05/12] Adds SiteGroup.comments in the required locations - [x] 1. Add the field to the model class - [x] 2. Generate and run database migrations - [NA] 3. Add validation logic to clean() - [NA] 4. Update relevant querysets - [x] 5. Update API serializer - [x] 6. Add fields to forms - [x] dcim.forms.model_forms.LocationForm, create/edit (e.g. model_forms.py) - [x] dcim.forms.buld_edit.LocationBulkEditForm, bulk edit - [x] dcim.dorms.bulk_import.LocationImportForm, CSV import - [x] filter (UI and API) - [x] 7. Extend object filter set - [x] 8. Add column to object table - [x] 9. Update the SearchIndex - [x] 10. Update the UI templates - [x] 11. Create/extend test cases - [NA] models - [x] views - [NA] forms - [x] filtersets - [x] api - [x] 12. Update the model's documentation --- netbox/dcim/api/serializers_/sites.py | 2 +- netbox/dcim/filtersets.py | 9 +++++++++ netbox/dcim/forms/bulk_edit.py | 3 ++- netbox/dcim/forms/bulk_import.py | 2 +- netbox/dcim/forms/model_forms.py | 3 ++- netbox/dcim/search.py | 1 + netbox/dcim/tables/sites.py | 4 ++-- netbox/dcim/tests/test_api.py | 8 ++++++-- netbox/dcim/tests/test_filtersets.py | 15 +++++++++++++-- netbox/dcim/tests/test_views.py | 20 +++++++++++--------- netbox/templates/dcim/sitegroup.html | 1 + 11 files changed, 49 insertions(+), 19 deletions(-) diff --git a/netbox/dcim/api/serializers_/sites.py b/netbox/dcim/api/serializers_/sites.py index 70924de5d..90f7b5d35 100644 --- a/netbox/dcim/api/serializers_/sites.py +++ b/netbox/dcim/api/serializers_/sites.py @@ -41,7 +41,7 @@ class SiteGroupSerializer(NestedGroupModelSerializer): model = SiteGroup 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') diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 6517d277a..6fb6bf980 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -149,6 +149,15 @@ class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet): model = SiteGroup fields = ('id', 'name', 'slug', 'description') + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(name__icontains=value) | + Q(description__icontains=value) | + Q(comments__icontains=value) + ).distinct() + class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet): status = django_filters.MultipleChoiceFilter( diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index dd78c0b23..c1da9c8d1 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -98,12 +98,13 @@ class SiteGroupBulkEditForm(NetBoxModelBulkEditForm): max_length=200, required=False ) + comments = CommentField() model = SiteGroup fieldsets = ( FieldSet('parent', 'description'), ) - nullable_fields = ('parent', 'description') + nullable_fields = ('parent', 'description', 'comments') class SiteBulkEditForm(NetBoxModelBulkEditForm): diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index cf9726360..469e40217 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -82,7 +82,7 @@ class SiteGroupImportForm(NetBoxModelImportForm): class Meta: model = SiteGroup - fields = ('name', 'slug', 'parent', 'description') + fields = ('name', 'slug', 'parent', 'description', 'comments', 'tags') class SiteImportForm(NetBoxModelImportForm): diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 639eebe13..dea031b64 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -98,6 +98,7 @@ class SiteGroupForm(NetBoxModelForm): required=False ) slug = SlugField() + comments = CommentField() fieldsets = ( FieldSet('parent', 'name', 'slug', 'description', 'tags'), @@ -106,7 +107,7 @@ class SiteGroupForm(NetBoxModelForm): class Meta: model = SiteGroup fields = ( - 'parent', 'name', 'slug', 'description', 'tags', + 'parent', 'name', 'slug', 'description', 'comments', 'tags', ) diff --git a/netbox/dcim/search.py b/netbox/dcim/search.py index e13c97ba7..a85005679 100644 --- a/netbox/dcim/search.py +++ b/netbox/dcim/search.py @@ -345,6 +345,7 @@ class SiteGroupIndex(SearchIndex): ('name', 100), ('slug', 110), ('description', 500), + ('comments', 5000), ) display_attrs = ('parent', 'description') diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py index 51e67f2f3..cc4e00e7e 100644 --- a/netbox/dcim/tables/sites.py +++ b/netbox/dcim/tables/sites.py @@ -63,8 +63,8 @@ class SiteGroupTable(ContactsColumnMixin, NetBoxTable): class Meta(NetBoxTable.Meta): model = SiteGroup 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') diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 68cf34fe4..807ac77d4 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -105,26 +105,30 @@ class SiteGroupTest(APIViewTestCases.APIViewTestCase): { 'name': 'Site Group 4', 'slug': 'site-group-4', + 'comments': '', }, { 'name': 'Site Group 5', 'slug': 'site-group-5', + 'comments': 'not actually empty', }, { 'name': 'Site Group 6', 'slug': 'site-group-6', + 'comments': 'Do I really exist?', }, ] bulk_update_data = { 'description': 'New description', + 'comments': 'I do exist!', } @classmethod def setUpTestData(cls): 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): diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index ebc9f3fba..0c4bbbaff 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -161,13 +161,17 @@ class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests): 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 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: site_group.save() 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 2A1', slug='site-group-2a1', parent=groups[2]), SiteGroup(name='Site Group 2B1', slug='site-group-2b1', parent=groups[3]), @@ -181,6 +185,13 @@ class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'q': 'foobar1'} 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): params = {'name': ['Site Group 1', 'Site Group 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 24fa4ae55..83effa188 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -73,7 +73,7 @@ class SiteGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase): # Create three 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 3', slug='site-group-3'), ) @@ -88,24 +88,26 @@ class SiteGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase): 'parent': sitegroups[2].pk, 'description': 'A new site group', 'tags': [t.pk for t in tags], + 'comments': 'still here', } 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 = ( - "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 = { 'description': 'New description', + 'comments': 'the end', } diff --git a/netbox/templates/dcim/sitegroup.html b/netbox/templates/dcim/sitegroup.html index 3ae43f210..9beb7c505 100644 --- a/netbox/templates/dcim/sitegroup.html +++ b/netbox/templates/dcim/sitegroup.html @@ -41,6 +41,7 @@
{% include 'inc/panels/tags.html' %} {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/comments.html' %} {% plugin_left_page object %}
From b8352260ee7dcf8be80281d7f45b5dc87096d3ca Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Tue, 11 Mar 2025 10:42:59 -0500 Subject: [PATCH 06/12] Adds ContactGroup.comments in the required locations - [x] 1. Add the field to the model class - [x] 2. Generate and run database migrations - [NA] 3. Add validation logic to clean() - [NA] 4. Update relevant querysets - [x] 5. Update API serializer - [x] 6. Add fields to forms - [x] tenancy.forms.model_forms, create/edit (e.g. model_forms.py) - [x] tenancy.forms.buld_edit, bulk edit - [x] tenancy.dorms.bulk_import, CSV import - [NA] filter (UI and API) - [x] 7. Extend object filter set - [x] 8. Add column to object table - [x] 9. Update the SearchIndex - [x] 10. Update the UI templates - [x] 11. Create/extend test cases - [NA] models - [x] views - [NA] forms - [x] filtersets - [x] api - [NA] 12. Update the model's documentation --- netbox/templates/tenancy/contactgroup.html | 1 + netbox/tenancy/api/serializers_/contacts.py | 2 +- netbox/tenancy/filtersets.py | 10 ++++++++++ netbox/tenancy/forms/bulk_edit.py | 3 ++- netbox/tenancy/forms/bulk_import.py | 2 +- netbox/tenancy/forms/model_forms.py | 3 ++- netbox/tenancy/search.py | 1 + netbox/tenancy/tables/contacts.py | 3 ++- netbox/tenancy/tests/test_api.py | 11 +++++++++-- netbox/tenancy/tests/test_filtersets.py | 17 ++++++++++++++--- netbox/tenancy/tests/test_views.py | 20 +++++++++++--------- 11 files changed, 54 insertions(+), 19 deletions(-) diff --git a/netbox/templates/tenancy/contactgroup.html b/netbox/templates/tenancy/contactgroup.html index bf6928c15..25b1da440 100644 --- a/netbox/templates/tenancy/contactgroup.html +++ b/netbox/templates/tenancy/contactgroup.html @@ -32,6 +32,7 @@
{% include 'inc/panels/tags.html' %} + {% include 'inc/panels/comments.html' %} {% plugin_left_page object %}
diff --git a/netbox/tenancy/api/serializers_/contacts.py b/netbox/tenancy/api/serializers_/contacts.py index 8c24df734..846e618b4 100644 --- a/netbox/tenancy/api/serializers_/contacts.py +++ b/netbox/tenancy/api/serializers_/contacts.py @@ -26,7 +26,7 @@ class ContactGroupSerializer(NestedGroupModelSerializer): model = ContactGroup 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') diff --git a/netbox/tenancy/filtersets.py b/netbox/tenancy/filtersets.py index e2de18231..ff5563f1a 100644 --- a/netbox/tenancy/filtersets.py +++ b/netbox/tenancy/filtersets.py @@ -51,6 +51,16 @@ class ContactGroupFilterSet(OrganizationalModelFilterSet): model = ContactGroup fields = ('id', 'name', 'slug', 'description') + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(name__icontains=value) | + Q(slug__icontains=value) | + Q(description__icontains=value) | + Q(comments__icontains=value) + ) + class ContactRoleFilterSet(OrganizationalModelFilterSet): diff --git a/netbox/tenancy/forms/bulk_edit.py b/netbox/tenancy/forms/bulk_edit.py index 5af3f22ac..a8528aec8 100644 --- a/netbox/tenancy/forms/bulk_edit.py +++ b/netbox/tenancy/forms/bulk_edit.py @@ -67,12 +67,13 @@ class ContactGroupBulkEditForm(NetBoxModelBulkEditForm): max_length=200, required=False ) + comments = CommentField() model = ContactGroup fieldsets = ( FieldSet('parent', 'description'), ) - nullable_fields = ('parent', 'description') + nullable_fields = ('parent', 'description', 'comments') class ContactRoleBulkEditForm(NetBoxModelBulkEditForm): diff --git a/netbox/tenancy/forms/bulk_import.py b/netbox/tenancy/forms/bulk_import.py index f37317549..d227cef14 100644 --- a/netbox/tenancy/forms/bulk_import.py +++ b/netbox/tenancy/forms/bulk_import.py @@ -65,7 +65,7 @@ class ContactGroupImportForm(NetBoxModelImportForm): class Meta: model = ContactGroup - fields = ('name', 'slug', 'parent', 'description', 'tags') + fields = ('name', 'slug', 'parent', 'description', 'tags', 'comments') class ContactRoleImportForm(NetBoxModelImportForm): diff --git a/netbox/tenancy/forms/model_forms.py b/netbox/tenancy/forms/model_forms.py index bc18deed6..d65d47f1f 100644 --- a/netbox/tenancy/forms/model_forms.py +++ b/netbox/tenancy/forms/model_forms.py @@ -70,6 +70,7 @@ class ContactGroupForm(NetBoxModelForm): required=False ) slug = SlugField() + comments = CommentField() fieldsets = ( FieldSet('parent', 'name', 'slug', 'description', 'tags', name=_('Contact Group')), @@ -77,7 +78,7 @@ class ContactGroupForm(NetBoxModelForm): class Meta: model = ContactGroup - fields = ('parent', 'name', 'slug', 'description', 'tags') + fields = ('parent', 'name', 'slug', 'description', 'tags', 'comments') class ContactRoleForm(NetBoxModelForm): diff --git a/netbox/tenancy/search.py b/netbox/tenancy/search.py index 56903d6b1..5050114a6 100644 --- a/netbox/tenancy/search.py +++ b/netbox/tenancy/search.py @@ -25,6 +25,7 @@ class ContactGroupIndex(SearchIndex): ('name', 100), ('slug', 110), ('description', 500), + ('comments', 5000), ) display_attrs = ('description',) diff --git a/netbox/tenancy/tables/contacts.py b/netbox/tenancy/tables/contacts.py index c4e35ab1b..e8761720e 100644 --- a/netbox/tenancy/tables/contacts.py +++ b/netbox/tenancy/tables/contacts.py @@ -31,7 +31,8 @@ class ContactGroupTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = ContactGroup 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') diff --git a/netbox/tenancy/tests/test_api.py b/netbox/tenancy/tests/test_api.py index c32ad3826..4dc33f943 100644 --- a/netbox/tenancy/tests/test_api.py +++ b/netbox/tenancy/tests/test_api.py @@ -107,13 +107,18 @@ class ContactGroupTest(APIViewTestCases.APIViewTestCase): def setUpTestData(cls): 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='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 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 = [ { @@ -125,11 +130,13 @@ class ContactGroupTest(APIViewTestCases.APIViewTestCase): 'name': 'Contact Group 5', 'slug': 'contact-group-5', 'parent': parent_contact_groups[1].pk, + 'comments': '', }, { 'name': 'Contact Group 6', 'slug': 'contact-group-6', 'parent': parent_contact_groups[1].pk, + 'comments': 'Child Group 6 comment', }, ] diff --git a/netbox/tenancy/tests/test_filtersets.py b/netbox/tenancy/tests/test_filtersets.py index f6890a3d4..97005dd1e 100644 --- a/netbox/tenancy/tests/test_filtersets.py +++ b/netbox/tenancy/tests/test_filtersets.py @@ -139,7 +139,7 @@ class ContactGroupTestCase(TestCase, ChangeLoggedFilterSetTests): parent_contact_groups = ( 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'), ) for contact_group in parent_contact_groups: @@ -162,14 +162,18 @@ class ContactGroupTestCase(TestCase, ChangeLoggedFilterSetTests): name='Contact Group 3A', slug='contact-group-3a', parent=parent_contact_groups[2], - description='foobar3' + description='foobar3', + comments='Contact Group 3A comment, not a parent', ), ) for contact_group in contact_groups: contact_group.save() 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 3A1', slug='contact-group-3a1', parent=contact_groups[2]), ) @@ -180,6 +184,13 @@ class ContactGroupTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'q': 'foobar1'} 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): params = {'name': ['Contact Group 1', 'Contact Group 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/tenancy/tests/test_views.py b/netbox/tenancy/tests/test_views.py index cbdecc0d0..b67ea428b 100644 --- a/netbox/tenancy/tests/test_views.py +++ b/netbox/tenancy/tests/test_views.py @@ -106,7 +106,7 @@ class ContactGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase): def setUpTestData(cls): 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 3', slug='contact-group-3'), ) @@ -120,24 +120,26 @@ class ContactGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase): 'slug': 'contact-group-x', 'description': 'A new contact group', 'tags': [t.pk for t in tags], + 'comments': 'Form data comment', } 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 = ( - "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 = { 'description': 'New description', + 'comments': 'Bulk update comment', } From 157df20ad4354428d81da32ab8a36321b5e0318a Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Tue, 11 Mar 2025 11:07:05 -0500 Subject: [PATCH 07/12] Adds TenantGroup.comments to the required locations - [x] 1. Add the field to the model class - [x] 2. Generate and run database migrations - [NA] 3. Add validation logic to clean() - [NA] 4. Update relevant querysets - [x] 5. Update API serializer - [x] 6. Add fields to forms - [x] tenancy.forms.model_forms, create/edit (e.g. model_forms.py) - [x] tenancy.forms.bulk_edit, bulk edit - [x] tenancy.forms.bulk_import, CSV import - [NA] filter (UI and API) - [x] 7. Extend object filter set - [x] 8. Add column to object table - [x] 9. Update the SearchIndex - [x] 10. Update the UI templates - [x] 11. Create/extend test cases - [NA] models - [x] views - [NA] forms - [x] filtersets - [x] api - [NA] 12. Update the model's documentation --- netbox/templates/tenancy/tenantgroup.html | 1 + netbox/tenancy/api/serializers_/tenants.py | 2 +- netbox/tenancy/filtersets.py | 10 ++++++++++ netbox/tenancy/forms/bulk_edit.py | 3 ++- netbox/tenancy/forms/bulk_import.py | 2 +- netbox/tenancy/forms/model_forms.py | 3 ++- netbox/tenancy/search.py | 1 + netbox/tenancy/tables/tenants.py | 3 ++- netbox/tenancy/tests/test_api.py | 11 +++++++++-- netbox/tenancy/tests/test_filtersets.py | 17 ++++++++++++++--- netbox/tenancy/tests/test_views.py | 20 +++++++++++--------- 11 files changed, 54 insertions(+), 19 deletions(-) diff --git a/netbox/templates/tenancy/tenantgroup.html b/netbox/templates/tenancy/tenantgroup.html index 0567f2ab3..ecf95a024 100644 --- a/netbox/templates/tenancy/tenantgroup.html +++ b/netbox/templates/tenancy/tenantgroup.html @@ -40,6 +40,7 @@
{% include 'inc/panels/tags.html' %} + {% include 'inc/panels/comments.html' %} {% plugin_left_page object %}
diff --git a/netbox/tenancy/api/serializers_/tenants.py b/netbox/tenancy/api/serializers_/tenants.py index 54e906f1d..189397c70 100644 --- a/netbox/tenancy/api/serializers_/tenants.py +++ b/netbox/tenancy/api/serializers_/tenants.py @@ -19,7 +19,7 @@ class TenantGroupSerializer(NestedGroupModelSerializer): model = TenantGroup 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') diff --git a/netbox/tenancy/filtersets.py b/netbox/tenancy/filtersets.py index ff5563f1a..c70b381ee 100644 --- a/netbox/tenancy/filtersets.py +++ b/netbox/tenancy/filtersets.py @@ -202,6 +202,16 @@ class TenantGroupFilterSet(OrganizationalModelFilterSet): model = TenantGroup fields = ('id', 'name', 'slug', 'description') + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(name__icontains=value) | + Q(slug__icontains=value) | + Q(description__icontains=value) | + Q(comments__icontains=value) + ) + class TenantFilterSet(NetBoxModelFilterSet, ContactModelFilterSet): group_id = TreeNodeMultipleChoiceFilter( diff --git a/netbox/tenancy/forms/bulk_edit.py b/netbox/tenancy/forms/bulk_edit.py index a8528aec8..3f72a30c1 100644 --- a/netbox/tenancy/forms/bulk_edit.py +++ b/netbox/tenancy/forms/bulk_edit.py @@ -33,9 +33,10 @@ class TenantGroupBulkEditForm(NetBoxModelBulkEditForm): max_length=200, required=False ) + comments = CommentField() model = TenantGroup - nullable_fields = ('parent', 'description') + nullable_fields = ('parent', 'description', 'comments') class TenantBulkEditForm(NetBoxModelBulkEditForm): diff --git a/netbox/tenancy/forms/bulk_import.py b/netbox/tenancy/forms/bulk_import.py index d227cef14..61c56a70f 100644 --- a/netbox/tenancy/forms/bulk_import.py +++ b/netbox/tenancy/forms/bulk_import.py @@ -31,7 +31,7 @@ class TenantGroupImportForm(NetBoxModelImportForm): class Meta: model = TenantGroup - fields = ('name', 'slug', 'parent', 'description', 'tags') + fields = ('name', 'slug', 'parent', 'description', 'tags', 'comments') class TenantImportForm(NetBoxModelImportForm): diff --git a/netbox/tenancy/forms/model_forms.py b/netbox/tenancy/forms/model_forms.py index d65d47f1f..e31a28416 100644 --- a/netbox/tenancy/forms/model_forms.py +++ b/netbox/tenancy/forms/model_forms.py @@ -27,6 +27,7 @@ class TenantGroupForm(NetBoxModelForm): required=False ) slug = SlugField() + comments = CommentField() fieldsets = ( FieldSet('parent', 'name', 'slug', 'description', 'tags', name=_('Tenant Group')), @@ -35,7 +36,7 @@ class TenantGroupForm(NetBoxModelForm): class Meta: model = TenantGroup fields = [ - 'parent', 'name', 'slug', 'description', 'tags', + 'parent', 'name', 'slug', 'description', 'tags', 'comments' ] diff --git a/netbox/tenancy/search.py b/netbox/tenancy/search.py index 5050114a6..f9441c974 100644 --- a/netbox/tenancy/search.py +++ b/netbox/tenancy/search.py @@ -60,5 +60,6 @@ class TenantGroupIndex(SearchIndex): ('name', 100), ('slug', 110), ('description', 500), + ('comments', 5000), ) display_attrs = ('description',) diff --git a/netbox/tenancy/tables/tenants.py b/netbox/tenancy/tables/tenants.py index a10133a64..8c73fb5a6 100644 --- a/netbox/tenancy/tables/tenants.py +++ b/netbox/tenancy/tables/tenants.py @@ -28,7 +28,8 @@ class TenantGroupTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = TenantGroup 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') diff --git a/netbox/tenancy/tests/test_api.py b/netbox/tenancy/tests/test_api.py index 4dc33f943..1804e0a8e 100644 --- a/netbox/tenancy/tests/test_api.py +++ b/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'] bulk_update_data = { 'description': 'New description', + 'comments': 'New Comment', } @classmethod @@ -28,12 +29,17 @@ class TenantGroupTest(APIViewTestCases.APIViewTestCase): parent_tenant_groups = ( 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 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 = [ { @@ -50,6 +56,7 @@ class TenantGroupTest(APIViewTestCases.APIViewTestCase): 'name': 'Tenant Group 6', 'slug': 'tenant-group-6', 'parent': parent_tenant_groups[1].pk, + 'comments': 'Tenant Group 6 comment', }, ] diff --git a/netbox/tenancy/tests/test_filtersets.py b/netbox/tenancy/tests/test_filtersets.py index 97005dd1e..7d44ee45d 100644 --- a/netbox/tenancy/tests/test_filtersets.py +++ b/netbox/tenancy/tests/test_filtersets.py @@ -16,7 +16,7 @@ class TenantGroupTestCase(TestCase, ChangeLoggedFilterSetTests): parent_tenant_groups = ( 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'), ) for tenant_group in parent_tenant_groups: @@ -27,7 +27,8 @@ class TenantGroupTestCase(TestCase, ChangeLoggedFilterSetTests): name='Tenant Group 1A', slug='tenant-group-1a', parent=parent_tenant_groups[0], - description='foobar1' + description='foobar1', + comments='Tenant Group 1A comment', ), TenantGroup( name='Tenant Group 2A', @@ -48,7 +49,10 @@ class TenantGroupTestCase(TestCase, ChangeLoggedFilterSetTests): child_tenant_groups = ( 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 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: tenant_group.save() @@ -57,6 +61,13 @@ class TenantGroupTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'q': 'foobar1'} 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): params = {'name': ['Tenant Group 1', 'Tenant Group 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/tenancy/tests/test_views.py b/netbox/tenancy/tests/test_views.py index b67ea428b..726c9ad97 100644 --- a/netbox/tenancy/tests/test_views.py +++ b/netbox/tenancy/tests/test_views.py @@ -15,7 +15,7 @@ class TenantGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase): tenant_groups = ( 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'), ) for tenanantgroup in tenant_groups: @@ -28,24 +28,26 @@ class TenantGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase): 'slug': 'tenant-group-x', 'description': 'A new tenant group', 'tags': [t.pk for t in tags], + 'comments': 'Tenant Group X comment', } 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 = ( - "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 = { 'description': 'New description', + 'comments': 'New comment', } From c0b019b735b612d3b2c9c123eb35cdb6a724ffa3 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Tue, 11 Mar 2025 11:51:25 -0500 Subject: [PATCH 08/12] Adds WirelessLANGroup.comments to all the required places - [x] 1. Add the field to the model class - [x] 2. Generate and run database migrations - [NA] 3. Add validation logic to clean() - [NA] 4. Update relevant querysets - [x] 5. Update API serializer - [x] 6. Add fields to forms - [x] wireless.forms.model_forms, create/edit (e.g. model_forms.py) - [x] wireless.forms.bulk_edit, bulk edit - [x] wireless.forms.bulk_import, CSV import - [NA] filter (UI and API) - [x] 7. Extend object filter set - [NA] 8. Add column to object table (Note: was already present) - [x] 9. Update the SearchIndex - [x] 10. Update the UI templates - [x] 11. Create/extend test cases - [NA] models - [x] views - [NA] forms - [x] filtersets - [x] api - [NA] 12. Update the model's documentation --- .../templates/wireless/wirelesslangroup.html | 1 + .../wireless/api/serializers_/wirelesslans.py | 2 +- netbox/wireless/filtersets.py | 10 +++++++++ netbox/wireless/forms/bulk_edit.py | 3 ++- netbox/wireless/forms/bulk_import.py | 2 +- netbox/wireless/forms/model_forms.py | 3 ++- netbox/wireless/search.py | 1 + netbox/wireless/tests/test_api.py | 3 +++ netbox/wireless/tests/test_filtersets.py | 19 ++++++++++++++-- netbox/wireless/tests/test_views.py | 22 +++++++++++-------- 10 files changed, 51 insertions(+), 15 deletions(-) diff --git a/netbox/templates/wireless/wirelesslangroup.html b/netbox/templates/wireless/wirelesslangroup.html index cb08b1b52..913e9da4c 100644 --- a/netbox/templates/wireless/wirelesslangroup.html +++ b/netbox/templates/wireless/wirelesslangroup.html @@ -40,6 +40,7 @@
{% include 'inc/panels/tags.html' %} + {% include 'inc/panels/comments.html' %} {% plugin_left_page object %}
diff --git a/netbox/wireless/api/serializers_/wirelesslans.py b/netbox/wireless/api/serializers_/wirelesslans.py index 68f79daf6..97d57f9f5 100644 --- a/netbox/wireless/api/serializers_/wirelesslans.py +++ b/netbox/wireless/api/serializers_/wirelesslans.py @@ -26,7 +26,7 @@ class WirelessLANGroupSerializer(NestedGroupModelSerializer): model = WirelessLANGroup 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') diff --git a/netbox/wireless/filtersets.py b/netbox/wireless/filtersets.py index cc5aefbd8..17ef66c0a 100644 --- a/netbox/wireless/filtersets.py +++ b/netbox/wireless/filtersets.py @@ -43,6 +43,16 @@ class WirelessLANGroupFilterSet(OrganizationalModelFilterSet): model = WirelessLANGroup fields = ('id', 'name', 'slug', 'description') + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(name__icontains=value) | + Q(slug__icontains=value) | + Q(description__icontains=value) | + Q(comments__icontains=value) + ) + class WirelessLANFilterSet(NetBoxModelFilterSet, ScopedFilterSet, TenancyFilterSet): group_id = TreeNodeMultipleChoiceFilter( diff --git a/netbox/wireless/forms/bulk_edit.py b/netbox/wireless/forms/bulk_edit.py index 5cd3a157a..1a75512e1 100644 --- a/netbox/wireless/forms/bulk_edit.py +++ b/netbox/wireless/forms/bulk_edit.py @@ -32,12 +32,13 @@ class WirelessLANGroupBulkEditForm(NetBoxModelBulkEditForm): max_length=200, required=False ) + comments = CommentField() model = WirelessLANGroup fieldsets = ( FieldSet('parent', 'description'), ) - nullable_fields = ('parent', 'description') + nullable_fields = ('parent', 'description', 'comments') class WirelessLANBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm): diff --git a/netbox/wireless/forms/bulk_import.py b/netbox/wireless/forms/bulk_import.py index 1fece7e46..389dcf25d 100644 --- a/netbox/wireless/forms/bulk_import.py +++ b/netbox/wireless/forms/bulk_import.py @@ -30,7 +30,7 @@ class WirelessLANGroupImportForm(NetBoxModelImportForm): class Meta: model = WirelessLANGroup - fields = ('name', 'slug', 'parent', 'description', 'tags') + fields = ('name', 'slug', 'parent', 'description', 'tags', 'comments') class WirelessLANImportForm(ScopedImportForm, NetBoxModelImportForm): diff --git a/netbox/wireless/forms/model_forms.py b/netbox/wireless/forms/model_forms.py index 9cfcca7ba..56422ab57 100644 --- a/netbox/wireless/forms/model_forms.py +++ b/netbox/wireless/forms/model_forms.py @@ -24,6 +24,7 @@ class WirelessLANGroupForm(NetBoxModelForm): required=False ) slug = SlugField() + comments = CommentField() fieldsets = ( FieldSet('parent', 'name', 'slug', 'description', 'tags', name=_('Wireless LAN Group')), @@ -32,7 +33,7 @@ class WirelessLANGroupForm(NetBoxModelForm): class Meta: model = WirelessLANGroup fields = [ - 'parent', 'name', 'slug', 'description', 'tags', + 'parent', 'name', 'slug', 'description', 'tags', 'comments', ] diff --git a/netbox/wireless/search.py b/netbox/wireless/search.py index e1be53c09..3c1565cb7 100644 --- a/netbox/wireless/search.py +++ b/netbox/wireless/search.py @@ -21,6 +21,7 @@ class WirelessLANGroupIndex(SearchIndex): ('name', 100), ('slug', 110), ('description', 500), + ('comments', 5000), ) display_attrs = ('description',) diff --git a/netbox/wireless/tests/test_api.py b/netbox/wireless/tests/test_api.py index f768eafaf..0fe5e45f6 100644 --- a/netbox/wireless/tests/test_api.py +++ b/netbox/wireless/tests/test_api.py @@ -24,10 +24,12 @@ class WirelessLANGroupTest(APIViewTestCases.APIViewTestCase): { 'name': 'Wireless LAN Group 4', 'slug': 'wireless-lan-group-4', + 'comments': '', }, { 'name': 'Wireless LAN Group 5', 'slug': 'wireless-lan-group-5', + 'comments': 'LAN Group 5 comment', }, { 'name': 'Wireless LAN Group 6', @@ -36,6 +38,7 @@ class WirelessLANGroupTest(APIViewTestCases.APIViewTestCase): ] bulk_update_data = { 'description': 'New description', + 'comments': 'New comment', } @classmethod diff --git a/netbox/wireless/tests/test_filtersets.py b/netbox/wireless/tests/test_filtersets.py index 27aab83d8..9e8905d4a 100644 --- a/netbox/wireless/tests/test_filtersets.py +++ b/netbox/wireless/tests/test_filtersets.py @@ -21,7 +21,10 @@ class WirelessLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests): parent_groups = ( 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 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: group.save() @@ -38,10 +41,15 @@ class WirelessLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests): slug='wireless-lan-group-1b', parent=parent_groups[0], 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 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]), ) for group in groups: @@ -62,6 +70,13 @@ class WirelessLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'q': 'foobar1'} 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): params = {'name': ['Wireless LAN Group 1', 'Wireless LAN Group 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/wireless/tests/test_views.py b/netbox/wireless/tests/test_views.py index 51af37364..975f18c0d 100644 --- a/netbox/wireless/tests/test_views.py +++ b/netbox/wireless/tests/test_views.py @@ -16,7 +16,9 @@ class WirelessLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase): groups = ( 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'), ) for group in groups: @@ -30,24 +32,26 @@ class WirelessLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase): 'parent': groups[2].pk, 'description': 'A new wireless LAN group', 'tags': [t.pk for t in tags], + 'comments': 'LAN Group X comment', } 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 = ( - "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 = { 'description': 'New description', + 'comments': 'New Comments', } From 1ea6f6e2ce14407226596cb2b596578314f143eb Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Tue, 11 Mar 2025 11:57:48 -0500 Subject: [PATCH 09/12] Ensures that all new comments fields render Markdown in tables --- netbox/dcim/tables/sites.py | 9 +++++++++ netbox/tenancy/tables/contacts.py | 3 +++ netbox/tenancy/tables/tenants.py | 3 +++ 3 files changed, 15 insertions(+) diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py index cc4e00e7e..7d2f0e0cc 100644 --- a/netbox/dcim/tables/sites.py +++ b/netbox/dcim/tables/sites.py @@ -32,6 +32,9 @@ class RegionTable(ContactsColumnMixin, NetBoxTable): tags = columns.TagColumn( url_name='dcim:region_list' ) + comments = columns.MarkdownColumn( + verbose_name=_('Comments'), + ) class Meta(NetBoxTable.Meta): model = Region @@ -59,6 +62,9 @@ class SiteGroupTable(ContactsColumnMixin, NetBoxTable): tags = columns.TagColumn( url_name='dcim:sitegroup_list' ) + comments = columns.MarkdownColumn( + verbose_name=_('Comments'), + ) class Meta(NetBoxTable.Meta): model = SiteGroup @@ -153,6 +159,9 @@ class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): actions = columns.ActionsColumn( extra_buttons=LOCATION_BUTTONS ) + comments = columns.MarkdownColumn( + verbose_name=_('Comments'), + ) class Meta(NetBoxTable.Meta): model = Location diff --git a/netbox/tenancy/tables/contacts.py b/netbox/tenancy/tables/contacts.py index e8761720e..ded6315ea 100644 --- a/netbox/tenancy/tables/contacts.py +++ b/netbox/tenancy/tables/contacts.py @@ -27,6 +27,9 @@ class ContactGroupTable(NetBoxTable): tags = columns.TagColumn( url_name='tenancy:contactgroup_list' ) + comments = columns.MarkdownColumn( + verbose_name=_('Comments'), + ) class Meta(NetBoxTable.Meta): model = ContactGroup diff --git a/netbox/tenancy/tables/tenants.py b/netbox/tenancy/tables/tenants.py index 8c73fb5a6..70f263dbe 100644 --- a/netbox/tenancy/tables/tenants.py +++ b/netbox/tenancy/tables/tenants.py @@ -24,6 +24,9 @@ class TenantGroupTable(NetBoxTable): tags = columns.TagColumn( url_name='tenancy:tenantgroup_list' ) + comments = columns.MarkdownColumn( + verbose_name=_('Comments'), + ) class Meta(NetBoxTable.Meta): model = TenantGroup From 2df68e29c9b9944a8ed48ae04671d9391081a3c2 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Tue, 11 Mar 2025 12:00:45 -0500 Subject: [PATCH 10/12] Ensures overridden filterset search() methods include fields from OrganizationalModelFilterSet --- netbox/dcim/filtersets.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 6fb6bf980..443a2dc36 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -115,6 +115,7 @@ class RegionFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet): return queryset return queryset.filter( Q(name__icontains=value) | + Q(slug__icontains=value) | Q(description__icontains=value) | Q(comments__icontains=value) ).distinct() @@ -154,6 +155,7 @@ class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet): return queryset return queryset.filter( Q(name__icontains=value) | + Q(slug__icontains=value) | Q(description__icontains=value) | Q(comments__icontains=value) ).distinct() @@ -297,6 +299,7 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM return queryset return queryset.filter( Q(name__icontains=value) | + Q(slug__icontains=value) | Q(facility__icontains=value) | Q(description__icontains=value) | Q(comments__icontains=value) From 06a206ee33853ae07a6f29067f0cd74ce13efe62 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Thu, 13 Mar 2025 15:36:55 -0500 Subject: [PATCH 11/12] Extract base NestedGroupModelFilterSet with base search behavior This can easily be extended (as in the case of LocationFilterSet) by calling super() and ORing a filter to the queryset that is returned. See: https://docs.djangoproject.com/en/5.1/ref/models/querysets/#or --- netbox/dcim/filtersets.py | 45 ++++++++++------------------------- netbox/netbox/filtersets.py | 16 +++++++++++++ netbox/tenancy/filtersets.py | 26 +++----------------- netbox/wireless/filtersets.py | 14 ++--------- 4 files changed, 33 insertions(+), 68 deletions(-) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 443a2dc36..6f9f481c3 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -11,7 +11,8 @@ from ipam.filtersets import PrimaryIPFilterSet from ipam.models import ASN, IPAddress, VLANTranslationPolicy, VRF from netbox.choices import ColorChoices from netbox.filtersets import ( - BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet, + BaseFilterSet, ChangeLoggedModelFilterSet, NestedGroupModelFilterSet, NetBoxModelFilterSet, + OrganizationalModelFilterSet, ) from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet from tenancy.models import * @@ -81,7 +82,7 @@ __all__ = ( ) -class RegionFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet): +class RegionFilterSet(NestedGroupModelFilterSet, ContactModelFilterSet): parent_id = django_filters.ModelMultipleChoiceFilter( queryset=Region.objects.all(), label=_('Parent region (ID)'), @@ -110,18 +111,8 @@ class RegionFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet): model = Region fields = ('id', 'name', 'slug', 'description') - def search(self, queryset, name, value): - if not value.strip(): - return queryset - return queryset.filter( - Q(name__icontains=value) | - Q(slug__icontains=value) | - Q(description__icontains=value) | - Q(comments__icontains=value) - ).distinct() - -class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet): +class SiteGroupFilterSet(NestedGroupModelFilterSet, ContactModelFilterSet): parent_id = django_filters.ModelMultipleChoiceFilter( queryset=SiteGroup.objects.all(), label=_('Parent site group (ID)'), @@ -150,16 +141,6 @@ class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet): model = SiteGroup fields = ('id', 'name', 'slug', 'description') - def search(self, queryset, name, value): - if not value.strip(): - return queryset - return queryset.filter( - Q(name__icontains=value) | - Q(slug__icontains=value) | - Q(description__icontains=value) | - Q(comments__icontains=value) - ).distinct() - class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet): status = django_filters.MultipleChoiceFilter( @@ -225,7 +206,7 @@ class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe return queryset.filter(qs_filter).distinct() -class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalModelFilterSet): +class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, NestedGroupModelFilterSet): region_id = TreeNodeMultipleChoiceFilter( queryset=Region.objects.all(), field_name='site__region', @@ -295,15 +276,13 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM fields = ('id', 'name', 'slug', 'facility', 'description') def search(self, queryset, name, value): - if not value.strip(): - return queryset - return queryset.filter( - Q(name__icontains=value) | - Q(slug__icontains=value) | - Q(facility__icontains=value) | - Q(description__icontains=value) | - Q(comments__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): diff --git a/netbox/netbox/filtersets.py b/netbox/netbox/filtersets.py index b8fbe7ad5..d80b07e90 100644 --- a/netbox/netbox/filtersets.py +++ b/netbox/netbox/filtersets.py @@ -329,3 +329,19 @@ class OrganizationalModelFilterSet(NetBoxModelFilterSet): models.Q(slug__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 diff --git a/netbox/tenancy/filtersets.py b/netbox/tenancy/filtersets.py index c70b381ee..db7236abe 100644 --- a/netbox/tenancy/filtersets.py +++ b/netbox/tenancy/filtersets.py @@ -2,7 +2,7 @@ import django_filters from django.db.models import Q 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 .models import * @@ -22,7 +22,7 @@ __all__ = ( # Contacts # -class ContactGroupFilterSet(OrganizationalModelFilterSet): +class ContactGroupFilterSet(NestedGroupModelFilterSet): parent_id = django_filters.ModelMultipleChoiceFilter( queryset=ContactGroup.objects.all(), label=_('Parent contact group (ID)'), @@ -51,16 +51,6 @@ class ContactGroupFilterSet(OrganizationalModelFilterSet): model = ContactGroup fields = ('id', 'name', 'slug', 'description') - def search(self, queryset, name, value): - if not value.strip(): - return queryset - return queryset.filter( - Q(name__icontains=value) | - Q(slug__icontains=value) | - Q(description__icontains=value) | - Q(comments__icontains=value) - ) - class ContactRoleFilterSet(OrganizationalModelFilterSet): @@ -173,7 +163,7 @@ class ContactModelFilterSet(django_filters.FilterSet): # Tenancy # -class TenantGroupFilterSet(OrganizationalModelFilterSet): +class TenantGroupFilterSet(NestedGroupModelFilterSet): parent_id = django_filters.ModelMultipleChoiceFilter( queryset=TenantGroup.objects.all(), label=_('Parent tenant group (ID)'), @@ -202,16 +192,6 @@ class TenantGroupFilterSet(OrganizationalModelFilterSet): model = TenantGroup fields = ('id', 'name', 'slug', 'description') - def search(self, queryset, name, value): - if not value.strip(): - return queryset - return queryset.filter( - Q(name__icontains=value) | - Q(slug__icontains=value) | - Q(description__icontains=value) | - Q(comments__icontains=value) - ) - class TenantFilterSet(NetBoxModelFilterSet, ContactModelFilterSet): group_id = TreeNodeMultipleChoiceFilter( diff --git a/netbox/wireless/filtersets.py b/netbox/wireless/filtersets.py index 17ef66c0a..bd96865ad 100644 --- a/netbox/wireless/filtersets.py +++ b/netbox/wireless/filtersets.py @@ -5,7 +5,7 @@ from dcim.choices import LinkStatusChoices from dcim.base_filtersets import ScopedFilterSet from dcim.models import Interface from ipam.models import VLAN -from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet +from netbox.filtersets import NestedGroupModelFilterSet, NetBoxModelFilterSet from tenancy.filtersets import TenancyFilterSet from utilities.filters import TreeNodeMultipleChoiceFilter from .choices import * @@ -18,7 +18,7 @@ __all__ = ( ) -class WirelessLANGroupFilterSet(OrganizationalModelFilterSet): +class WirelessLANGroupFilterSet(NestedGroupModelFilterSet): parent_id = django_filters.ModelMultipleChoiceFilter( queryset=WirelessLANGroup.objects.all() ) @@ -43,16 +43,6 @@ class WirelessLANGroupFilterSet(OrganizationalModelFilterSet): model = WirelessLANGroup fields = ('id', 'name', 'slug', 'description') - def search(self, queryset, name, value): - if not value.strip(): - return queryset - return queryset.filter( - Q(name__icontains=value) | - Q(slug__icontains=value) | - Q(description__icontains=value) | - Q(comments__icontains=value) - ) - class WirelessLANFilterSet(NetBoxModelFilterSet, ScopedFilterSet, TenancyFilterSet): group_id = TreeNodeMultipleChoiceFilter( From b45e256f27670e6abae516f72bce35720d92995f Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Thu, 13 Mar 2025 15:43:32 -0500 Subject: [PATCH 12/12] Removes banner from new migrations --- ...0202_location_comments_region_comments_sitegroup_comments.py | 2 -- .../0018_contactgroup_comments_tenantgroup_comments.py | 2 -- netbox/wireless/migrations/0014_wirelesslangroup_comments.py | 2 -- 3 files changed, 6 deletions(-) diff --git a/netbox/dcim/migrations/0202_location_comments_region_comments_sitegroup_comments.py b/netbox/dcim/migrations/0202_location_comments_region_comments_sitegroup_comments.py index 51031de53..ffdc5ba8a 100644 --- a/netbox/dcim/migrations/0202_location_comments_region_comments_sitegroup_comments.py +++ b/netbox/dcim/migrations/0202_location_comments_region_comments_sitegroup_comments.py @@ -1,5 +1,3 @@ -# Generated by Django 5.1.7 on 2025-03-10 16:37 - from django.db import migrations, models diff --git a/netbox/tenancy/migrations/0018_contactgroup_comments_tenantgroup_comments.py b/netbox/tenancy/migrations/0018_contactgroup_comments_tenantgroup_comments.py index 3481baeec..5f6a95149 100644 --- a/netbox/tenancy/migrations/0018_contactgroup_comments_tenantgroup_comments.py +++ b/netbox/tenancy/migrations/0018_contactgroup_comments_tenantgroup_comments.py @@ -1,5 +1,3 @@ -# Generated by Django 5.1.7 on 2025-03-10 16:37 - from django.db import migrations, models diff --git a/netbox/wireless/migrations/0014_wirelesslangroup_comments.py b/netbox/wireless/migrations/0014_wirelesslangroup_comments.py index 3e3cab270..9fc1a99d6 100644 --- a/netbox/wireless/migrations/0014_wirelesslangroup_comments.py +++ b/netbox/wireless/migrations/0014_wirelesslangroup_comments.py @@ -1,5 +1,3 @@ -# Generated by Django 5.1.7 on 2025-03-10 16:37 - from django.db import migrations, models