Browse Source

15106 Add Length Field to Wireless Link (#16528)

* 15106 add wireles link length

* 15106 add wireles link length

* 15106 add wireless link length

* 15106 add tests

* 15106 rename length -> distance

* 15106 rename length -> distance

* 15106 review comments

* 15106 review comments

* 15106 fix form

* 15106 length -> distance
Arthur Hanson 1 year ago
parent
commit
91dcecbd07

+ 4 - 0
docs/models/wireless/wirelesslink.md

@@ -40,3 +40,7 @@ The security cipher used to apply wireless authentication. Options include:
 ### Pre-Shared Key
 
 The security key configured on each client to grant access to the secured wireless LAN. This applies only to certain authentication types.
+
+### Distance
+
+The numeric distance of the link, including a unit designation (e.g. 100 meters or 25 feet).

+ 0 - 5
netbox/dcim/forms/model_forms.py

@@ -656,11 +656,6 @@ class CableForm(TenancyForm, NetBoxModelForm):
             'a_terminations_type', 'b_terminations_type', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color',
             'length', 'length_unit', 'description', 'comments', 'tags',
         ]
-        error_messages = {
-            'length': {
-                'max_value': _('Maximum length is 32767 (any unit)')
-            }
-        }
 
 
 class PowerPanelForm(NetBoxModelForm):

+ 2 - 0
netbox/dcim/svg/cables.py

@@ -393,6 +393,8 @@ class CableTraceSVG:
                         labels = [f"{cable}"] if len(links) > 2 else [f"Wireless {cable}", cable.get_status_display()]
                         if cable.ssid:
                             description.append(f"{cable.ssid}")
+                        if cable.distance and cable.distance_unit:
+                            description.append(f"{cable.distance} {cable.get_distance_unit_display()}")
                         near = [term for term in near_terminations if term.object == cable.interface_a]
                         far = [term for term in far_terminations if term.object == cable.interface_b]
                         if not (near and far):

+ 1 - 1
netbox/dcim/tables/cables.py

@@ -109,7 +109,7 @@ class CableTable(TenancyColumnsMixin, NetBoxTable):
     status = columns.ChoiceFieldColumn()
     length = columns.TemplateColumn(
         template_code=CABLE_LENGTH,
-        order_by=('_abs_length', 'length_unit')
+        order_by=('_abs_length')
     )
     color = columns.ColorColumn()
     comments = columns.MarkdownColumn()

+ 10 - 0
netbox/templates/wireless/wirelesslink.html

@@ -34,6 +34,16 @@
             <th scope="row">{% trans "Description" %}</th>
             <td>{{ object.description|placeholder }}</td>
           </tr>
+          <tr>
+            <th scope="row">{% trans "Distance" %}</th>
+            <td>
+              {% if object.distance is not None %}
+                {{ object.distance|floatformat }} {{ object.get_distance_unit_display }}
+              {% else %}
+                {{ ''|placeholder }}
+              {% endif %}
+            </td>
+          </tr>
         </table>
       </div>
       {% include 'inc/panels/tags.html' %}

+ 3 - 1
netbox/wireless/api/serializers_/wirelesslinks.py

@@ -21,11 +21,13 @@ class WirelessLinkSerializer(NetBoxModelSerializer):
     tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True)
     auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True)
+    distance_unit = ChoiceField(choices=WirelessLinkDistanceUnitChoices, allow_blank=True, required=False, allow_null=True)
 
     class Meta:
         model = WirelessLink
         fields = [
             'id', 'url', 'display', 'interface_a', 'interface_b', 'ssid', 'status', 'tenant', 'auth_type',
-            'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
+            'auth_cipher', 'auth_psk', 'distance', 'distance_unit', 'description', 'comments', 'tags', 'custom_fields',
+            'created', 'last_updated',
         ]
         brief_fields = ('id', 'url', 'display', 'ssid', 'description')

+ 18 - 0
netbox/wireless/choices.py

@@ -481,3 +481,21 @@ class WirelessAuthCipherChoices(ChoiceSet):
         (CIPHER_TKIP, 'TKIP'),
         (CIPHER_AES, 'AES'),
     )
+
+
+class WirelessLinkDistanceUnitChoices(ChoiceSet):
+
+    # Metric
+    UNIT_KILOMETER = 'km'
+    UNIT_METER = 'm'
+
+    # Imperial
+    UNIT_MILE = 'mi'
+    UNIT_FOOT = 'ft'
+
+    CHOICES = (
+        (UNIT_KILOMETER, _('Kilometers')),
+        (UNIT_METER, _('Meters')),
+        (UNIT_MILE, _('Miles')),
+        (UNIT_FOOT, _('Feet')),
+    )

+ 1 - 1
netbox/wireless/filtersets.py

@@ -105,7 +105,7 @@ class WirelessLinkFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
 
     class Meta:
         model = WirelessLink
-        fields = ('id', 'ssid', 'auth_psk', 'description')
+        fields = ('id', 'ssid', 'auth_psk', 'distance', 'distance_unit', 'description')
 
     def search(self, queryset, name, value):
         if not value.strip():

+ 14 - 2
netbox/wireless/forms/bulk_edit.py

@@ -125,6 +125,17 @@ class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm):
         required=False,
         label=_('Pre-shared key')
     )
+    distance = forms.DecimalField(
+        label=_('Distance'),
+        min_value=0,
+        required=False
+    )
+    distance_unit = forms.ChoiceField(
+        label=_('Distance unit'),
+        choices=add_blank_choice(WirelessLinkDistanceUnitChoices),
+        required=False,
+        initial=''
+    )
     description = forms.CharField(
         label=_('Description'),
         max_length=200,
@@ -135,8 +146,9 @@ class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm):
     model = WirelessLink
     fieldsets = (
         FieldSet('ssid', 'status', 'tenant', 'description'),
-        FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication'))
+        FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
+        FieldSet('distance', 'distance_unit', name=_('Attributes')),
     )
     nullable_fields = (
-        'ssid', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'comments',
+        'ssid', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'distance', 'comments',
     )

+ 8 - 2
netbox/wireless/forms/bulk_import.py

@@ -112,10 +112,16 @@ class WirelessLinkImportForm(NetBoxModelImportForm):
         required=False,
         help_text=_('Authentication cipher')
     )
+    distance_unit = CSVChoiceField(
+        label=_('Distance unit'),
+        choices=WirelessLinkDistanceUnitChoices,
+        required=False,
+        help_text=_('Distance unit')
+    )
 
     class Meta:
         model = WirelessLink
         fields = (
-            'interface_a', 'interface_b', 'ssid', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'description',
-            'comments', 'tags',
+            'interface_a', 'interface_b', 'ssid', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk',
+            'distance', 'distance_unit', 'description', 'comments', 'tags',
         )

+ 10 - 1
netbox/wireless/forms/filtersets.py

@@ -71,7 +71,7 @@ class WirelessLinkFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = WirelessLink
     fieldsets = (
         FieldSet('q', 'filter_id', 'tag'),
-        FieldSet('ssid', 'status', name=_('Attributes')),
+        FieldSet('ssid', 'status', 'distance', 'distance_unit', name=_('Attributes')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
     )
@@ -98,4 +98,13 @@ class WirelessLinkFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
         label=_('Pre-shared key'),
         required=False
     )
+    distance = forms.DecimalField(
+        label=_('Distance'),
+        required=False,
+    )
+    distance_unit = forms.ChoiceField(
+        label=_('Distance unit'),
+        choices=add_blank_choice(WirelessLinkDistanceUnitChoices),
+        required=False
+    )
     tag = TagFilterField(model)

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

@@ -159,7 +159,7 @@ class WirelessLinkForm(TenancyForm, NetBoxModelForm):
     fieldsets = (
         FieldSet('site_a', 'location_a', 'device_a', 'interface_a', name=_('Side A')),
         FieldSet('site_b', 'location_b', 'device_b', 'interface_b', name=_('Side B')),
-        FieldSet('status', 'ssid', 'description', 'tags', name=_('Link')),
+        FieldSet('status', 'ssid', 'distance', 'distance_unit', 'description', 'tags', name=_('Link')),
         FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
         FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
     )
@@ -168,8 +168,8 @@ class WirelessLinkForm(TenancyForm, NetBoxModelForm):
         model = WirelessLink
         fields = [
             'site_a', 'location_a', 'device_a', 'interface_a', 'site_b', 'location_b', 'device_b', 'interface_b',
-            'status', 'ssid', 'tenant_group', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'description',
-            'comments', 'tags',
+            'status', 'ssid', 'tenant_group', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk',
+            'distance', 'distance_unit', 'description', 'comments', 'tags',
         ]
         widgets = {
             'auth_psk': PasswordInput(

+ 28 - 0
netbox/wireless/migrations/0009_wirelesslink_distance.py

@@ -0,0 +1,28 @@
+# Generated by Django 5.0.6 on 2024-06-12 18:15
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('wireless', '0001_squashed_0008'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='wirelesslink',
+            name='_abs_distance',
+            field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True),
+        ),
+        migrations.AddField(
+            model_name='wirelesslink',
+            name='distance',
+            field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True),
+        ),
+        migrations.AddField(
+            model_name='wirelesslink',
+            name='distance_unit',
+            field=models.CharField(blank=True, max_length=50),
+        ),
+    ]

+ 35 - 0
netbox/wireless/models.py

@@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _
 from dcim.choices import LinkStatusChoices
 from dcim.constants import WIRELESS_IFACE_TYPES
 from netbox.models import NestedGroupModel, PrimaryModel
+from utilities.conversion import to_meters
 from .choices import *
 from .constants import *
 
@@ -160,6 +161,26 @@ class WirelessLink(WirelessAuthenticationBase, PrimaryModel):
         choices=LinkStatusChoices,
         default=LinkStatusChoices.STATUS_CONNECTED
     )
+    distance = models.DecimalField(
+        verbose_name=_('distance'),
+        max_digits=8,
+        decimal_places=2,
+        blank=True,
+        null=True
+    )
+    distance_unit = models.CharField(
+        verbose_name=_('distance unit'),
+        max_length=50,
+        choices=WirelessLinkDistanceUnitChoices,
+        blank=True,
+    )
+    # Stores the normalized distance (in meters) for database ordering
+    _abs_distance = models.DecimalField(
+        max_digits=10,
+        decimal_places=4,
+        blank=True,
+        null=True
+    )
     tenant = models.ForeignKey(
         to='tenancy.Tenant',
         on_delete=models.PROTECT,
@@ -208,6 +229,11 @@ class WirelessLink(WirelessAuthenticationBase, PrimaryModel):
         return LinkStatusChoices.colors.get(self.status)
 
     def clean(self):
+        super().clean()
+
+        # Validate distance and distance_unit
+        if self.distance is not None and not self.distance_unit:
+            raise ValidationError(_("Must specify a unit when setting a wireless distance"))
 
         # Validate interface types
         if self.interface_a.type not in WIRELESS_IFACE_TYPES:
@@ -224,6 +250,15 @@ class WirelessLink(WirelessAuthenticationBase, PrimaryModel):
             })
 
     def save(self, *args, **kwargs):
+        # Store the given distance (if any) in meters for use in database ordering
+        if self.distance is not None and self.distance_unit:
+            self._abs_distance = to_meters(self.distance, self.distance_unit)
+        else:
+            self._abs_distance = None
+
+        # Clear distance_unit if no distance is defined
+        if self.distance is None:
+            self.distance_unit = ''
 
         # Store the parent Device for the A and B interfaces
         self._interface_a_device = self.interface_a.device

+ 4 - 0
netbox/wireless/tables/template_code.py

@@ -0,0 +1,4 @@
+WIRELESS_LINK_DISTANCE = """
+{% load helpers %}
+{% if record.distance %}{{ record.distance|floatformat:"-2" }} {{ record.distance_unit }}{% endif %}
+"""

+ 7 - 1
netbox/wireless/tables/wirelesslink.py

@@ -4,6 +4,7 @@ import django_tables2 as tables
 from netbox.tables import NetBoxTable, columns
 from tenancy.tables import TenancyColumnsMixin
 from wireless.models import *
+from .template_code import WIRELESS_LINK_DISTANCE
 
 __all__ = (
     'WirelessLinkTable',
@@ -36,6 +37,10 @@ class WirelessLinkTable(TenancyColumnsMixin, NetBoxTable):
         verbose_name=_('Interface B'),
         linkify=True
     )
+    distance = columns.TemplateColumn(
+        template_code=WIRELESS_LINK_DISTANCE,
+        order_by=('_abs_distance')
+    )
     tags = columns.TagColumn(
         url_name='wireless:wirelesslink_list'
     )
@@ -44,7 +49,8 @@ class WirelessLinkTable(TenancyColumnsMixin, NetBoxTable):
         model = WirelessLink
         fields = (
             'pk', 'id', 'status', 'device_a', 'interface_a', 'device_b', 'interface_b', 'ssid', 'tenant',
-            'tenant_group', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'tags', 'created', 'last_updated',
+            'tenant_group', 'distance', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'tags',
+            'created', 'last_updated',
         )
         default_columns = (
             'pk', 'id', 'status', 'device_a', 'interface_a', 'device_b', 'interface_b', 'ssid', 'auth_type',

+ 2 - 0
netbox/wireless/tests/test_api.py

@@ -113,6 +113,8 @@ class WirelessLinkTest(APIViewTestCases.APIViewTestCase):
     brief_fields = ['description', 'display', 'id', 'ssid', 'url']
     bulk_update_data = {
         'status': 'planned',
+        'distance': 100,
+        'distance_unit': 'm',
     }
 
     @classmethod

+ 14 - 0
netbox/wireless/tests/test_filtersets.py

@@ -260,6 +260,8 @@ class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
             auth_cipher=WirelessAuthCipherChoices.CIPHER_AUTO,
             auth_psk='PSK1',
             tenant=tenants[0],
+            distance=10,
+            distance_unit=WirelessLinkDistanceUnitChoices.UNIT_FOOT,
             description='foobar1'
         ).save()
         WirelessLink(
@@ -271,6 +273,8 @@ class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
             auth_cipher=WirelessAuthCipherChoices.CIPHER_TKIP,
             auth_psk='PSK2',
             tenant=tenants[1],
+            distance=20,
+            distance_unit=WirelessLinkDistanceUnitChoices.UNIT_METER,
             description='foobar2'
         ).save()
         WirelessLink(
@@ -281,6 +285,8 @@ class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
             auth_type=WirelessAuthTypeChoices.TYPE_WPA_PERSONAL,
             auth_cipher=WirelessAuthCipherChoices.CIPHER_AES,
             auth_psk='PSK3',
+            distance=30,
+            distance_unit=WirelessLinkDistanceUnitChoices.UNIT_METER,
             tenant=tenants[2],
         ).save()
         WirelessLink(
@@ -313,6 +319,14 @@ class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'auth_psk': ['PSK1', 'PSK2']}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
+    def test_distance(self):
+        params = {'distance': [10, 20]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_distance_unit(self):
+        params = {'distance_unit': WirelessLinkDistanceUnitChoices.UNIT_FOOT}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
     def test_description(self):
         params = {'description': ['foobar1', 'foobar2']}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

+ 4 - 0
netbox/wireless/tests/test_views.py

@@ -160,6 +160,8 @@ class WirelessLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             'interface_a': interfaces[6].pk,
             'interface_b': interfaces[7].pk,
             'status': LinkStatusChoices.STATUS_PLANNED,
+            'distance': 100,
+            'distance_unit': WirelessLinkDistanceUnitChoices.UNIT_FOOT,
             'tenant': tenants[1].pk,
             'tags': [t.pk for t in tags],
         }
@@ -180,4 +182,6 @@ class WirelessLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
 
         cls.bulk_edit_data = {
             'status': LinkStatusChoices.STATUS_PLANNED,
+            'distance': 50,
+            'distance_unit': WirelessLinkDistanceUnitChoices.UNIT_METER,
         }