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
This commit is contained in:
Jason Novinger 2025-03-10 14:53:23 -05:00
parent ae7a47ca60
commit 44efd5e833
10 changed files with 42 additions and 14 deletions

View File

@ -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')

View File

@ -280,7 +280,8 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM
return queryset.filter( return queryset.filter(
Q(name__icontains=value) | Q(name__icontains=value) |
Q(facility__icontains=value) | Q(facility__icontains=value) |
Q(description__icontains=value) Q(description__icontains=value) |
Q(comments__icontains=value)
) )

View File

@ -197,12 +197,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):

View File

@ -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)

View File

@ -179,6 +179,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 +189,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',
) )

View File

@ -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')

View File

@ -158,7 +158,7 @@ class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
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'

View File

@ -212,12 +212,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 +229,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 +253,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 +261,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',

View File

@ -401,6 +401,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 +411,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 +421,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 +439,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)

View File

@ -202,6 +202,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 +210,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 +218,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 +235,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,tenant,name,slug,status,description,comments",
"Site 1,Tenant 1,Location 4,location-4,planned,Fourth location", "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 5,location-5,planned,Fifth location,",
"Site 1,Tenant 1,Location 6,location-6,planned,Sixth location", "Site 1,Tenant 1,Location 6,location-6,planned,Sixth location,hi!",
) )
cls.csv_update_data = ( cls.csv_update_data = (
"id,name,description", "id,name,description,comments",
f"{locations[0].pk},Location 7,Fourth location7", f"{locations[0].pk},Location 7,Fourth location7,Useful comment",
f"{locations[1].pk},Location 8,Fifth location8", f"{locations[1].pk},Location 8,Fifth location8,unuseful comment",
f"{locations[2].pk},Location 0,Sixth location9", 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',
} }