From 4f76dcd2ea856fa9411dbf287696501f59d6a6c1 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Wed, 14 Jun 2023 11:18:50 -0700 Subject: [PATCH] 11305 Add GPS coordinates to device (#12782) * 11305 add lat/long to devices * 11305 update docs * 11305 update tests --- docs/models/dcim/device.md | 4 ++++ netbox/dcim/api/serializers.py | 7 +++--- netbox/dcim/filtersets.py | 2 +- netbox/dcim/forms/bulk_import.py | 5 +++-- netbox/dcim/forms/model_forms.py | 6 ++--- .../0174_device_latitude_device_longitude.py | 22 +++++++++++++++++++ netbox/dcim/models/devices.py | 14 ++++++++++++ netbox/dcim/tables/devices.py | 6 ++--- netbox/dcim/tests/test_filtersets.py | 14 +++++++++--- netbox/dcim/tests/test_views.py | 2 ++ netbox/templates/dcim/device.html | 17 ++++++++++++++ netbox/templates/dcim/device_edit.html | 2 ++ 12 files changed, 86 insertions(+), 15 deletions(-) create mode 100644 netbox/dcim/migrations/0174_device_latitude_device_longitude.py diff --git a/docs/models/dcim/device.md b/docs/models/dcim/device.md index 8f97b920b..2216e351c 100644 --- a/docs/models/dcim/device.md +++ b/docs/models/dcim/device.md @@ -61,6 +61,10 @@ If installed in a rack, this field indicates the base rack unit in which the dev !!! tip Devices with a height of more than one rack unit should be set to the lowest-numbered rack unit that they occupy. +### Latitude & Longitude + +GPS coordinates of the device for geolocation. + ### Status The device's operational status. diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 894a3f4f9..3a3065acc 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -673,9 +673,10 @@ class DeviceSerializer(NetBoxModelSerializer): model = Device fields = [ 'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', - 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip', - 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', - 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', + 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status', 'airflow', + 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', + 'description', 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', 'created', + 'last_updated', ] @extend_schema_field(NestedDeviceSerializer) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index d159e9b73..e87a37847 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -999,7 +999,7 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter class Meta: model = Device - fields = ['id', 'asset_tag', 'face', 'position', 'airflow', 'vc_position', 'vc_priority'] + fields = ['id', 'asset_tag', 'face', 'position', 'latitude', 'longitude', 'airflow', 'vc_position', 'vc_priority'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index c8f13e213..e3e97ab73 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -478,8 +478,9 @@ class DeviceImportForm(BaseDeviceImportForm): class Meta(BaseDeviceImportForm.Meta): fields = [ 'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status', - 'site', 'location', 'rack', 'position', 'face', 'parent', 'device_bay', 'airflow', 'virtual_chassis', - 'vc_position', 'vc_priority', 'cluster', 'description', 'config_template', 'comments', 'tags', + 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent', 'device_bay', 'airflow', + 'virtual_chassis', 'vc_position', 'vc_priority', 'cluster', 'description', 'config_template', 'comments', + 'tags', ] def __init__(self, data=None, *args, **kwargs): diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 8379fd085..56542d70c 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -449,9 +449,9 @@ class DeviceForm(TenancyForm, NetBoxModelForm): model = Device fields = [ 'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face', - 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'cluster', 'tenant_group', 'tenant', - 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template', 'comments', 'tags', - 'local_context_data' + 'latitude', 'longitude', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'cluster', + 'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template', + 'comments', 'tags', 'local_context_data' ] def __init__(self, *args, **kwargs): diff --git a/netbox/dcim/migrations/0174_device_latitude_device_longitude.py b/netbox/dcim/migrations/0174_device_latitude_device_longitude.py new file mode 100644 index 000000000..f9f72f9f8 --- /dev/null +++ b/netbox/dcim/migrations/0174_device_latitude_device_longitude.py @@ -0,0 +1,22 @@ +# Generated by Django 4.1.9 on 2023-05-31 22:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('dcim', '0173_remove_napalm_fields'), + ] + + operations = [ + migrations.AddField( + model_name='device', + name='latitude', + field=models.DecimalField(blank=True, decimal_places=6, max_digits=8, null=True), + ), + migrations.AddField( + model_name='device', + name='longitude', + field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), + ), + ] diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index a908a6ab6..30fafef94 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -624,6 +624,20 @@ class Device(PrimaryModel, ConfigContextModel): blank=True, null=True ) + latitude = models.DecimalField( + max_digits=8, + decimal_places=6, + blank=True, + null=True, + help_text=_("GPS coordinate in decimal format (xx.yyyyyy)") + ) + longitude = models.DecimalField( + max_digits=9, + decimal_places=6, + blank=True, + null=True, + help_text=_("GPS coordinate in decimal format (xx.yyyyyy)") + ) # Generic relations contacts = GenericRelation( diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index a0238a1fb..a5862da68 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -236,9 +236,9 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): fields = ( 'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'parent_device', - 'device_bay_position', 'position', 'face', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', - 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template', 'comments', 'contacts', - 'tags', 'created', 'last_updated', + 'device_bay_position', 'position', 'face', 'latitude', 'longitude', 'airflow', 'primary_ip', 'primary_ip4', + 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template', + 'comments', 'contacts', 'tags', 'created', 'last_updated', ) default_columns = ( 'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type', diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index bd0931d5a..aa6860a16 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -1638,9 +1638,9 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests): Tenant.objects.bulk_create(tenants) devices = ( - Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], platform=platforms[0], tenant=tenants[0], serial='ABC', asset_tag='1001', site=sites[0], location=locations[0], rack=racks[0], position=1, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_ACTIVE, cluster=clusters[0], local_context_data={"foo": 123}), - Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], platform=platforms[1], tenant=tenants[1], serial='DEF', asset_tag='1002', site=sites[1], location=locations[1], rack=racks[1], position=2, face=DeviceFaceChoices.FACE_FRONT, status=DeviceStatusChoices.STATUS_STAGED, airflow=DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR, cluster=clusters[1]), - Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], platform=platforms[2], tenant=tenants[2], serial='GHI', asset_tag='1003', site=sites[2], location=locations[2], rack=racks[2], position=3, face=DeviceFaceChoices.FACE_REAR, status=DeviceStatusChoices.STATUS_FAILED, airflow=DeviceAirflowChoices.AIRFLOW_REAR_TO_FRONT, cluster=clusters[2]), + Device(name='Device 1', device_type=device_types[0], device_role=device_roles[0], platform=platforms[0], tenant=tenants[0], serial='ABC', asset_tag='1001', site=sites[0], location=locations[0], rack=racks[0], position=1, face=DeviceFaceChoices.FACE_FRONT, latitude=10, longitude=10, status=DeviceStatusChoices.STATUS_ACTIVE, cluster=clusters[0], local_context_data={"foo": 123}), + Device(name='Device 2', device_type=device_types[1], device_role=device_roles[1], platform=platforms[1], tenant=tenants[1], serial='DEF', asset_tag='1002', site=sites[1], location=locations[1], rack=racks[1], position=2, face=DeviceFaceChoices.FACE_FRONT, latitude=20, longitude=20, status=DeviceStatusChoices.STATUS_STAGED, airflow=DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR, cluster=clusters[1]), + Device(name='Device 3', device_type=device_types[2], device_role=device_roles[2], platform=platforms[2], tenant=tenants[2], serial='GHI', asset_tag='1003', site=sites[2], location=locations[2], rack=racks[2], position=3, face=DeviceFaceChoices.FACE_REAR, latitude=30, longitude=30, status=DeviceStatusChoices.STATUS_FAILED, airflow=DeviceAirflowChoices.AIRFLOW_REAR_TO_FRONT, cluster=clusters[2]), ) Device.objects.bulk_create(devices) @@ -1721,6 +1721,14 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'position': [1, 2]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_latitude(self): + params = {'latitude': [10, 20]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_longitude(self): + params = {'longitude': [10, 20]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_vc_position(self): params = {'vc_position': [1, 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 4bcb8df53..a327d6400 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -1696,6 +1696,8 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'rack': racks[1].pk, 'position': 1, 'face': DeviceFaceChoices.FACE_FRONT, + 'latitude': Decimal('35.780000'), + 'longitude': Decimal('-78.642000'), 'status': DeviceStatusChoices.STATUS_PLANNED, 'primary_ip4': None, 'primary_ip6': None, diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index b0e67269c..68fa84a24 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -76,6 +76,23 @@ {% endif %} + + GPS Coordinates + + {% if object.latitude and object.longitude %} + {% if config.MAPS_URL %} +
+ + Map It + +
+ {% endif %} + {{ object.latitude }}, {{ object.longitude }} + {% else %} + {{ ''|placeholder }} + {% endif %} + + Tenant diff --git a/netbox/templates/dcim/device_edit.html b/netbox/templates/dcim/device_edit.html index 17780b513..2dbe1e3c5 100644 --- a/netbox/templates/dcim/device_edit.html +++ b/netbox/templates/dcim/device_edit.html @@ -53,6 +53,8 @@ {% else %} {% render_field form.face %} {% render_field form.position %} + {% render_field form.latitude %} + {% render_field form.longitude %} {% endif %}