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 = [
'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')

View File

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

View File

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

View File

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

View File

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

View File

@ -144,6 +144,7 @@ class LocationIndex(SearchIndex):
('facility', 100),
('slug', 110),
('description', 500),
('comments', 5000),
)
display_attrs = ('site', 'status', 'tenant', 'facility', 'description')

View File

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

View File

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

View File

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

View File

@ -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',
}