Преглед изворни кода

11305 Add GPS coordinates to device (#12782)

* 11305 add lat/long to devices

* 11305 update docs

* 11305 update tests
Arthur Hanson пре 2 година
родитељ
комит
4f76dcd2ea

+ 4 - 0
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
 !!! tip
     Devices with a height of more than one rack unit should be set to the lowest-numbered rack unit that they occupy.
     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
 ### Status
 
 
 The device's operational status.
 The device's operational status.

+ 4 - 3
netbox/dcim/api/serializers.py

@@ -673,9 +673,10 @@ class DeviceSerializer(NetBoxModelSerializer):
         model = Device
         model = Device
         fields = [
         fields = [
             'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
             '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)
     @extend_schema_field(NestedDeviceSerializer)

+ 1 - 1
netbox/dcim/filtersets.py

@@ -999,7 +999,7 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
 
 
     class Meta:
     class Meta:
         model = Device
         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):
     def search(self, queryset, name, value):
         if not value.strip():
         if not value.strip():

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

@@ -478,8 +478,9 @@ class DeviceImportForm(BaseDeviceImportForm):
     class Meta(BaseDeviceImportForm.Meta):
     class Meta(BaseDeviceImportForm.Meta):
         fields = [
         fields = [
             'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
             '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):
     def __init__(self, data=None, *args, **kwargs):

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

@@ -449,9 +449,9 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
         model = Device
         model = Device
         fields = [
         fields = [
             'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face',
             '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):
     def __init__(self, *args, **kwargs):

+ 22 - 0
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),
+        ),
+    ]

+ 14 - 0
netbox/dcim/models/devices.py

@@ -624,6 +624,20 @@ class Device(PrimaryModel, ConfigContextModel):
         blank=True,
         blank=True,
         null=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
     # Generic relations
     contacts = GenericRelation(
     contacts = GenericRelation(

+ 3 - 3
netbox/dcim/tables/devices.py

@@ -236,9 +236,9 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
         fields = (
         fields = (
             'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type',
             'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type',
             'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'parent_device',
             '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 = (
         default_columns = (
             'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',
             'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',

+ 11 - 3
netbox/dcim/tests/test_filtersets.py

@@ -1638,9 +1638,9 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
         Tenant.objects.bulk_create(tenants)
         Tenant.objects.bulk_create(tenants)
 
 
         devices = (
         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)
         Device.objects.bulk_create(devices)
 
 
@@ -1721,6 +1721,14 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'position': [1, 2]}
         params = {'position': [1, 2]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 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):
     def test_vc_position(self):
         params = {'vc_position': [1, 2]}
         params = {'vc_position': [1, 2]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

+ 2 - 0
netbox/dcim/tests/test_views.py

@@ -1696,6 +1696,8 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             'rack': racks[1].pk,
             'rack': racks[1].pk,
             'position': 1,
             'position': 1,
             'face': DeviceFaceChoices.FACE_FRONT,
             'face': DeviceFaceChoices.FACE_FRONT,
+            'latitude': Decimal('35.780000'),
+            'longitude': Decimal('-78.642000'),
             'status': DeviceStatusChoices.STATUS_PLANNED,
             'status': DeviceStatusChoices.STATUS_PLANNED,
             'primary_ip4': None,
             'primary_ip4': None,
             'primary_ip6': None,
             'primary_ip6': None,

+ 17 - 0
netbox/templates/dcim/device.html

@@ -76,6 +76,23 @@
                                 {% endif %}
                                 {% endif %}
                             </td>
                             </td>
                         </tr>
                         </tr>
+                        <tr>
+                          <th scope="row">GPS Coordinates</th>
+                          <td class="position-relative">
+                            {% if object.latitude and object.longitude %}
+                              {% if config.MAPS_URL %}
+                                <div class="position-absolute top-50 end-0 translate-middle-y noprint">
+                                  <a href="{{ config.MAPS_URL }}{{ object.latitude }},{{ object.longitude }}" target="_blank" class="btn btn-primary btn-sm">
+                                    <i class="mdi mdi-map-marker"></i> Map It
+                                  </a>
+                                </div>
+                                {% endif %}
+                              <span>{{ object.latitude }}, {{ object.longitude }}</span>
+                            {% else %}
+                              {{ ''|placeholder }}
+                            {% endif %}
+                          </td>
+                        </tr>
                         <tr>
                         <tr>
                             <th scope="row">Tenant</th>
                             <th scope="row">Tenant</th>
                             <td>
                             <td>

+ 2 - 0
netbox/templates/dcim/device_edit.html

@@ -53,6 +53,8 @@
       {% else %}
       {% else %}
         {% render_field form.face %}
         {% render_field form.face %}
         {% render_field form.position %}
         {% render_field form.position %}
+        {% render_field form.latitude %}
+        {% render_field form.longitude %}
       {% endif %}
       {% endif %}
     </div>
     </div>