소스 검색

16547 Add distance to Circuit (#17629)

* 16547 Add distance to Circuit

* 16547 fix test cases

* 16547 fix test cases

* 16547 add distance to API, forms, tables

* 16547 fixes

* 16547 fixes

* 16547 review changes

* 16547 review changes

* Clean up DistanceColumn

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
Arthur Hanson 1 년 전
부모
커밋
65687851fe
38개의 변경된 파일290개의 추가작업 그리고 150개의 파일을 삭제
  1. 6 0
      docs/models/circuits/circuit.md
  2. 3 1
      netbox/circuits/api/serializers_/circuits.py
  3. 1 1
      netbox/circuits/filtersets.py
  4. 13 0
      netbox/circuits/forms/bulk_edit.py
  5. 8 1
      netbox/circuits/forms/bulk_import.py
  6. 12 1
      netbox/circuits/forms/filtersets.py
  7. 13 3
      netbox/circuits/forms/model_forms.py
  8. 28 0
      netbox/circuits/migrations/0045_circuit_distance.py
  9. 2 1
      netbox/circuits/models/circuits.py
  10. 1 0
      netbox/circuits/tables/circuits.py
  11. 12 3
      netbox/circuits/tests/test_filtersets.py
  12. 1 0
      netbox/dcim/api/serializers_/devicetypes.py
  13. 1 0
      netbox/dcim/api/serializers_/racks.py
  14. 0 18
      netbox/dcim/choices.py
  15. 1 0
      netbox/dcim/forms/bulk_edit.py
  16. 1 0
      netbox/dcim/forms/bulk_import.py
  17. 1 0
      netbox/dcim/forms/filtersets.py
  18. 2 1
      netbox/dcim/models/devices.py
  19. 0 44
      netbox/dcim/models/mixins.py
  20. 1 1
      netbox/dcim/models/racks.py
  21. 1 1
      netbox/dcim/tests/test_filtersets.py
  22. 1 0
      netbox/dcim/tests/test_models.py
  23. 1 1
      netbox/dcim/tests/test_views.py
  24. 38 0
      netbox/netbox/choices.py
  25. 97 0
      netbox/netbox/models/mixins.py
  26. 14 0
      netbox/netbox/tables/columns.py
  27. 10 0
      netbox/templates/circuits/circuit.html
  28. 2 1
      netbox/utilities/conversion.py
  29. 2 1
      netbox/wireless/api/serializers_/wirelesslinks.py
  30. 0 18
      netbox/wireless/choices.py
  31. 2 1
      netbox/wireless/forms/bulk_edit.py
  32. 2 1
      netbox/wireless/forms/bulk_import.py
  33. 2 1
      netbox/wireless/forms/filtersets.py
  34. 2 35
      netbox/wireless/models.py
  35. 0 4
      netbox/wireless/tables/template_code.py
  36. 1 5
      netbox/wireless/tables/wirelesslink.py
  37. 5 4
      netbox/wireless/tests/test_filtersets.py
  38. 3 2
      netbox/wireless/tests/test_views.py

+ 6 - 0
docs/models/circuits/circuit.md

@@ -36,6 +36,12 @@ The operational status of the circuit. By default, the following statuses are av
 !!! tip "Custom circuit statuses"
 !!! tip "Custom circuit statuses"
     Additional circuit statuses may be defined by setting `Circuit.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
     Additional circuit statuses may be defined by setting `Circuit.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter.
 
 
+### Distance
+
+!!! info "This field was introduced in NetBox v4.2."
+
+The distance between the circuit's two endpoints, including a unit designation (e.g. 100 meters or 25 feet).
+
 ### Description
 ### Description
 
 
 A brief description of the circuit.
 A brief description of the circuit.

+ 3 - 1
netbox/circuits/api/serializers_/circuits.py

@@ -4,6 +4,7 @@ from dcim.api.serializers_.cables import CabledObjectSerializer
 from dcim.api.serializers_.sites import SiteSerializer
 from dcim.api.serializers_.sites import SiteSerializer
 from netbox.api.fields import ChoiceField, RelatedObjectCountField
 from netbox.api.fields import ChoiceField, RelatedObjectCountField
 from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
 from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
+from netbox.choices import DistanceUnitChoices
 from tenancy.api.serializers_.tenants import TenantSerializer
 from tenancy.api.serializers_.tenants import TenantSerializer
 
 
 from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer
 from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer
@@ -80,13 +81,14 @@ class CircuitSerializer(NetBoxModelSerializer):
     termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
     termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
     termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
     termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
     assignments = CircuitGroupAssignmentSerializer_(nested=True, many=True, required=False)
     assignments = CircuitGroupAssignmentSerializer_(nested=True, many=True, required=False)
+    distance_unit = ChoiceField(choices=DistanceUnitChoices, allow_blank=True, required=False, allow_null=True)
 
 
     class Meta:
     class Meta:
         model = Circuit
         model = Circuit
         fields = [
         fields = [
             'id', 'url', 'display_url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant',
             'id', 'url', 'display_url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant',
             'install_date', 'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z',
             'install_date', 'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z',
-            'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'assignments',
+            'distance', 'distance_unit', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'assignments',
         ]
         ]
         brief_fields = ('id', 'url', 'display', 'provider', 'cid', 'description')
         brief_fields = ('id', 'url', 'display', 'provider', 'cid', 'description')
 
 

+ 1 - 1
netbox/circuits/filtersets.py

@@ -239,7 +239,7 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
 
 
     class Meta:
     class Meta:
         model = Circuit
         model = Circuit
-        fields = ('id', 'cid', 'description', 'install_date', 'termination_date', 'commit_rate')
+        fields = ('id', 'cid', 'description', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit')
 
 
     def search(self, queryset, name, value):
     def search(self, queryset, name, value):
         if not value.strip():
         if not value.strip():

+ 13 - 0
netbox/circuits/forms/bulk_edit.py

@@ -5,6 +5,7 @@ from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, C
 from circuits.models import *
 from circuits.models import *
 from dcim.models import Site
 from dcim.models import Site
 from ipam.models import ASN
 from ipam.models import ASN
+from netbox.choices import DistanceUnitChoices
 from netbox.forms import NetBoxModelBulkEditForm
 from netbox.forms import NetBoxModelBulkEditForm
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms import add_blank_choice
 from utilities.forms import add_blank_choice
@@ -160,6 +161,17 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
             options=CircuitCommitRateChoices
             options=CircuitCommitRateChoices
         )
         )
     )
     )
+    distance = forms.DecimalField(
+        label=_('Distance'),
+        min_value=0,
+        required=False
+    )
+    distance_unit = forms.ChoiceField(
+        label=_('Distance unit'),
+        choices=add_blank_choice(DistanceUnitChoices),
+        required=False,
+        initial=''
+    )
     description = forms.CharField(
     description = forms.CharField(
         label=_('Description'),
         label=_('Description'),
         max_length=100,
         max_length=100,
@@ -171,6 +183,7 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
     fieldsets = (
     fieldsets = (
         FieldSet('provider', 'type', 'status', 'description', name=_('Circuit')),
         FieldSet('provider', 'type', 'status', 'description', name=_('Circuit')),
         FieldSet('provider_account', 'install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
         FieldSet('provider_account', 'install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
+        FieldSet('distance', 'distance_unit', name=_('Attributes')),
         FieldSet('tenant', name=_('Tenancy')),
         FieldSet('tenant', name=_('Tenancy')),
     )
     )
     nullable_fields = (
     nullable_fields = (

+ 8 - 1
netbox/circuits/forms/bulk_import.py

@@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _
 from circuits.choices import *
 from circuits.choices import *
 from circuits.models import *
 from circuits.models import *
 from dcim.models import Site
 from dcim.models import Site
+from netbox.choices import DistanceUnitChoices
 from netbox.forms import NetBoxModelImportForm
 from netbox.forms import NetBoxModelImportForm
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField
 from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField
@@ -95,6 +96,12 @@ class CircuitImportForm(NetBoxModelImportForm):
         choices=CircuitStatusChoices,
         choices=CircuitStatusChoices,
         help_text=_('Operational status')
         help_text=_('Operational status')
     )
     )
+    distance_unit = CSVChoiceField(
+        label=_('Distance unit'),
+        choices=DistanceUnitChoices,
+        required=False,
+        help_text=_('Distance unit')
+    )
     tenant = CSVModelChoiceField(
     tenant = CSVModelChoiceField(
         label=_('Tenant'),
         label=_('Tenant'),
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
@@ -107,7 +114,7 @@ class CircuitImportForm(NetBoxModelImportForm):
         model = Circuit
         model = Circuit
         fields = [
         fields = [
             'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date',
             'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date',
-            'commit_rate', 'description', 'comments', 'tags'
+            'commit_rate', 'distance', 'distance_unit', 'description', 'comments', 'tags'
         ]
         ]
 
 
 
 

+ 12 - 1
netbox/circuits/forms/filtersets.py

@@ -5,8 +5,10 @@ from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, C
 from circuits.models import *
 from circuits.models import *
 from dcim.models import Region, Site, SiteGroup
 from dcim.models import Region, Site, SiteGroup
 from ipam.models import ASN
 from ipam.models import ASN
+from netbox.choices import DistanceUnitChoices
 from netbox.forms import NetBoxModelFilterSetForm
 from netbox.forms import NetBoxModelFilterSetForm
 from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
 from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
+from utilities.forms import add_blank_choice
 from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
 from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
 from utilities.forms.rendering import FieldSet
 from utilities.forms.rendering import FieldSet
 from utilities.forms.widgets import DatePicker, NumberWithOptions
 from utilities.forms.widgets import DatePicker, NumberWithOptions
@@ -114,7 +116,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
     fieldsets = (
     fieldsets = (
         FieldSet('q', 'filter_id', 'tag'),
         FieldSet('q', 'filter_id', 'tag'),
         FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
         FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')),
-        FieldSet('type_id', 'status', 'install_date', 'termination_date', 'commit_rate', name=_('Attributes')),
+        FieldSet('type_id', 'status', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit', name=_('Attributes')),
         FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
         FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
         FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
         FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
@@ -188,6 +190,15 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
             options=CircuitCommitRateChoices
             options=CircuitCommitRateChoices
         )
         )
     )
     )
+    distance = forms.DecimalField(
+        label=_('Distance'),
+        required=False,
+    )
+    distance_unit = forms.ChoiceField(
+        label=_('Distance unit'),
+        choices=add_blank_choice(DistanceUnitChoices),
+        required=False
+    )
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 
 
 

+ 13 - 3
netbox/circuits/forms/model_forms.py

@@ -7,7 +7,7 @@ from ipam.models import ASN
 from netbox.forms import NetBoxModelForm
 from netbox.forms import NetBoxModelForm
 from tenancy.forms import TenancyForm
 from tenancy.forms import TenancyForm
 from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
 from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
-from utilities.forms.rendering import FieldSet, TabbedGroups
+from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
 from utilities.forms.widgets import DatePicker, NumberWithOptions
 from utilities.forms.widgets import DatePicker, NumberWithOptions
 
 
 __all__ = (
 __all__ = (
@@ -108,7 +108,17 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        FieldSet('provider', 'provider_account', 'cid', 'type', 'status', 'description', 'tags', name=_('Circuit')),
+        FieldSet(
+            'provider',
+            'provider_account',
+            'cid',
+            'type',
+            'status',
+            InlineFields('distance', 'distance_unit', label=_('Distance')),
+            'description',
+            'tags',
+            name=_('Circuit')
+        ),
         FieldSet('install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
         FieldSet('install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
         FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
         FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
     )
     )
@@ -117,7 +127,7 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
         model = Circuit
         model = Circuit
         fields = [
         fields = [
             'cid', 'type', 'provider', 'provider_account', 'status', 'install_date', 'termination_date', 'commit_rate',
             'cid', 'type', 'provider', 'provider_account', 'status', 'install_date', 'termination_date', 'commit_rate',
-            'description', 'tenant_group', 'tenant', 'comments', 'tags',
+            'distance', 'distance_unit', 'description', 'tenant_group', 'tenant', 'comments', 'tags',
         ]
         ]
         widgets = {
         widgets = {
             'install_date': DatePicker(),
             'install_date': DatePicker(),

+ 28 - 0
netbox/circuits/migrations/0045_circuit_distance.py

@@ -0,0 +1,28 @@
+# Generated by Django 5.0.9 on 2024-09-26 22:14
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('circuits', '0044_circuit_groups'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='circuit',
+            name='_abs_distance',
+            field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True),
+        ),
+        migrations.AddField(
+            model_name='circuit',
+            name='distance',
+            field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True),
+        ),
+        migrations.AddField(
+            model_name='circuit',
+            name='distance_unit',
+            field=models.CharField(blank=True, max_length=50),
+        ),
+    ]

+ 2 - 1
netbox/circuits/models/circuits.py

@@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _
 from circuits.choices import *
 from circuits.choices import *
 from dcim.models import CabledObjectModel
 from dcim.models import CabledObjectModel
 from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
 from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
+from netbox.models.mixins import DistanceMixin
 from netbox.models.features import ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, ImageAttachmentsMixin, TagsMixin
 from netbox.models.features import ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, ImageAttachmentsMixin, TagsMixin
 from utilities.fields import ColorField
 from utilities.fields import ColorField
 
 
@@ -34,7 +35,7 @@ class CircuitType(OrganizationalModel):
         verbose_name_plural = _('circuit types')
         verbose_name_plural = _('circuit types')
 
 
 
 
-class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
+class Circuit(ContactsMixin, ImageAttachmentsMixin, DistanceMixin, PrimaryModel):
     """
     """
     A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
     A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
     circuits. Each circuit is also assigned a CircuitType and a Site, and may optionally be assigned to a particular
     circuits. Each circuit is also assigned a CircuitType and a Site, and may optionally be assigned to a particular

+ 1 - 0
netbox/circuits/tables/circuits.py

@@ -76,6 +76,7 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
     commit_rate = CommitRateColumn(
     commit_rate = CommitRateColumn(
         verbose_name=_('Commit Rate')
         verbose_name=_('Commit Rate')
     )
     )
+    distance = columns.DistanceColumn()
     comments = columns.MarkdownColumn(
     comments = columns.MarkdownColumn(
         verbose_name=_('Comments')
         verbose_name=_('Comments')
     )
     )

+ 12 - 3
netbox/circuits/tests/test_filtersets.py

@@ -5,6 +5,7 @@ from circuits.filtersets import *
 from circuits.models import *
 from circuits.models import *
 from dcim.models import Cable, Region, Site, SiteGroup
 from dcim.models import Cable, Region, Site, SiteGroup
 from ipam.models import ASN, RIR
 from ipam.models import ASN, RIR
+from netbox.choices import DistanceUnitChoices
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
 from utilities.testing import ChangeLoggedFilterSetTests
 from utilities.testing import ChangeLoggedFilterSetTests
 
 
@@ -222,9 +223,9 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
         ProviderNetwork.objects.bulk_create(provider_networks)
         ProviderNetwork.objects.bulk_create(provider_networks)
 
 
         circuits = (
         circuits = (
-            Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', termination_date='2021-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar1'),
-            Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', termination_date='2021-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar2'),
-            Circuit(provider=providers[0], provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', termination_date='2021-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED),
+            Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', termination_date='2021-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar1', distance=10, distance_unit=DistanceUnitChoices.UNIT_FOOT),
+            Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', termination_date='2021-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar2', distance=20, distance_unit=DistanceUnitChoices.UNIT_METER),
+            Circuit(provider=providers[0], provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', termination_date='2021-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED, distance=30, distance_unit=DistanceUnitChoices.UNIT_METER),
             Circuit(provider=providers[1], provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[1], cid='Test Circuit 4', install_date='2020-01-04', termination_date='2021-01-04', commit_rate=4000, status=CircuitStatusChoices.STATUS_PLANNED),
             Circuit(provider=providers[1], provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[1], cid='Test Circuit 4', install_date='2020-01-04', termination_date='2021-01-04', commit_rate=4000, status=CircuitStatusChoices.STATUS_PLANNED),
             Circuit(provider=providers[1], provider_account=provider_accounts[2], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 5', install_date='2020-01-05', termination_date='2021-01-05', commit_rate=5000, status=CircuitStatusChoices.STATUS_OFFLINE),
             Circuit(provider=providers[1], provider_account=provider_accounts[2], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 5', install_date='2020-01-05', termination_date='2021-01-05', commit_rate=5000, status=CircuitStatusChoices.STATUS_OFFLINE),
             Circuit(provider=providers[1], provider_account=provider_accounts[2], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 6', install_date='2020-01-06', termination_date='2021-01-06', commit_rate=6000, status=CircuitStatusChoices.STATUS_OFFLINE),
             Circuit(provider=providers[1], provider_account=provider_accounts[2], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 6', install_date='2020-01-06', termination_date='2021-01-06', commit_rate=6000, status=CircuitStatusChoices.STATUS_OFFLINE),
@@ -289,6 +290,14 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]}
         params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
 
 
+    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': DistanceUnitChoices.UNIT_FOOT}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
     def test_description(self):
     def test_description(self):
         params = {'description': ['foobar1', 'foobar2']}
         params = {'description': ['foobar1', 'foobar2']}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

+ 1 - 0
netbox/dcim/api/serializers_/devicetypes.py

@@ -7,6 +7,7 @@ from dcim.choices import *
 from dcim.models import DeviceType, ModuleType
 from dcim.models import DeviceType, ModuleType
 from netbox.api.fields import ChoiceField, RelatedObjectCountField
 from netbox.api.fields import ChoiceField, RelatedObjectCountField
 from netbox.api.serializers import NetBoxModelSerializer
 from netbox.api.serializers import NetBoxModelSerializer
+from netbox.choices import *
 from .manufacturers import ManufacturerSerializer
 from .manufacturers import ManufacturerSerializer
 from .platforms import PlatformSerializer
 from .platforms import PlatformSerializer
 
 

+ 1 - 0
netbox/dcim/api/serializers_/racks.py

@@ -6,6 +6,7 @@ from dcim.constants import *
 from dcim.models import Rack, RackReservation, RackRole, RackType
 from dcim.models import Rack, RackReservation, RackRole, RackType
 from netbox.api.fields import ChoiceField, RelatedObjectCountField
 from netbox.api.fields import ChoiceField, RelatedObjectCountField
 from netbox.api.serializers import NetBoxModelSerializer
 from netbox.api.serializers import NetBoxModelSerializer
+from netbox.choices import *
 from netbox.config import ConfigItem
 from netbox.config import ConfigItem
 from tenancy.api.serializers_.tenants import TenantSerializer
 from tenancy.api.serializers_.tenants import TenantSerializer
 from users.api.serializers_.users import UserSerializer
 from users.api.serializers_.users import UserSerializer

+ 0 - 18
netbox/dcim/choices.py

@@ -1546,24 +1546,6 @@ class CableLengthUnitChoices(ChoiceSet):
     )
     )
 
 
 
 
-class WeightUnitChoices(ChoiceSet):
-
-    # Metric
-    UNIT_KILOGRAM = 'kg'
-    UNIT_GRAM = 'g'
-
-    # Imperial
-    UNIT_POUND = 'lb'
-    UNIT_OUNCE = 'oz'
-
-    CHOICES = (
-        (UNIT_KILOGRAM, _('Kilograms')),
-        (UNIT_GRAM, _('Grams')),
-        (UNIT_POUND, _('Pounds')),
-        (UNIT_OUNCE, _('Ounces')),
-    )
-
-
 #
 #
 # CableTerminations
 # CableTerminations
 #
 #

+ 1 - 0
netbox/dcim/forms/bulk_edit.py

@@ -8,6 +8,7 @@ from dcim.constants import *
 from dcim.models import *
 from dcim.models import *
 from extras.models import ConfigTemplate
 from extras.models import ConfigTemplate
 from ipam.models import ASN, VLAN, VLANGroup, VRF
 from ipam.models import ASN, VLAN, VLANGroup, VRF
+from netbox.choices import *
 from netbox.forms import NetBoxModelBulkEditForm
 from netbox.forms import NetBoxModelBulkEditForm
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from users.models import User
 from users.models import User

+ 1 - 0
netbox/dcim/forms/bulk_import.py

@@ -10,6 +10,7 @@ from dcim.constants import *
 from dcim.models import *
 from dcim.models import *
 from extras.models import ConfigTemplate
 from extras.models import ConfigTemplate
 from ipam.models import VRF, IPAddress
 from ipam.models import VRF, IPAddress
+from netbox.choices import *
 from netbox.forms import NetBoxModelImportForm
 from netbox.forms import NetBoxModelImportForm
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms.fields import (
 from utilities.forms.fields import (

+ 1 - 0
netbox/dcim/forms/filtersets.py

@@ -7,6 +7,7 @@ from dcim.models import *
 from extras.forms import LocalConfigContextFilterForm
 from extras.forms import LocalConfigContextFilterForm
 from extras.models import ConfigTemplate
 from extras.models import ConfigTemplate
 from ipam.models import ASN, VRF
 from ipam.models import ASN, VRF
+from netbox.choices import *
 from netbox.forms import NetBoxModelFilterSetForm
 from netbox.forms import NetBoxModelFilterSetForm
 from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
 from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
 from users.models import User
 from users.models import User

+ 2 - 1
netbox/dcim/models/devices.py

@@ -21,11 +21,12 @@ from extras.querysets import ConfigContextModelQuerySet
 from netbox.choices import ColorChoices
 from netbox.choices import ColorChoices
 from netbox.config import ConfigItem
 from netbox.config import ConfigItem
 from netbox.models import OrganizationalModel, PrimaryModel
 from netbox.models import OrganizationalModel, PrimaryModel
+from netbox.models.mixins import WeightMixin
 from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
 from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
 from utilities.fields import ColorField, CounterCacheField, NaturalOrderingField
 from utilities.fields import ColorField, CounterCacheField, NaturalOrderingField
 from utilities.tracking import TrackingModelMixin
 from utilities.tracking import TrackingModelMixin
 from .device_components import *
 from .device_components import *
-from .mixins import RenderConfigMixin, WeightMixin
+from .mixins import RenderConfigMixin
 
 
 
 
 __all__ = (
 __all__ = (

+ 0 - 44
netbox/dcim/models/mixins.py

@@ -1,56 +1,12 @@
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from django.db import models
 from django.db import models
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
-from dcim.choices import *
-from utilities.conversion import to_grams
 
 
 __all__ = (
 __all__ = (
     'RenderConfigMixin',
     'RenderConfigMixin',
-    'WeightMixin',
 )
 )
 
 
 
 
-class WeightMixin(models.Model):
-    weight = models.DecimalField(
-        verbose_name=_('weight'),
-        max_digits=8,
-        decimal_places=2,
-        blank=True,
-        null=True
-    )
-    weight_unit = models.CharField(
-        verbose_name=_('weight unit'),
-        max_length=50,
-        choices=WeightUnitChoices,
-        blank=True,
-    )
-    # Stores the normalized weight (in grams) for database ordering
-    _abs_weight = models.PositiveBigIntegerField(
-        blank=True,
-        null=True
-    )
-
-    class Meta:
-        abstract = True
-
-    def save(self, *args, **kwargs):
-
-        # Store the given weight (if any) in grams for use in database ordering
-        if self.weight and self.weight_unit:
-            self._abs_weight = to_grams(self.weight, self.weight_unit)
-        else:
-            self._abs_weight = None
-
-        super().save(*args, **kwargs)
-
-    def clean(self):
-        super().clean()
-
-        # Validate weight and weight_unit
-        if self.weight and not self.weight_unit:
-            raise ValidationError(_("Must specify a unit when setting a weight"))
-
-
 class RenderConfigMixin(models.Model):
 class RenderConfigMixin(models.Model):
     config_template = models.ForeignKey(
     config_template = models.ForeignKey(
         to='extras.ConfigTemplate',
         to='extras.ConfigTemplate',

+ 1 - 1
netbox/dcim/models/racks.py

@@ -16,13 +16,13 @@ from dcim.constants import *
 from dcim.svg import RackElevationSVG
 from dcim.svg import RackElevationSVG
 from netbox.choices import ColorChoices
 from netbox.choices import ColorChoices
 from netbox.models import OrganizationalModel, PrimaryModel
 from netbox.models import OrganizationalModel, PrimaryModel
+from netbox.models.mixins import WeightMixin
 from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
 from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
 from utilities.conversion import to_grams
 from utilities.conversion import to_grams
 from utilities.data import array_to_string, drange
 from utilities.data import array_to_string, drange
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.fields import ColorField, NaturalOrderingField
 from .device_components import PowerPort
 from .device_components import PowerPort
 from .devices import Device, Module
 from .devices import Device, Module
-from .mixins import WeightMixin
 from .power import PowerFeed
 from .power import PowerFeed
 
 
 __all__ = (
 __all__ = (

+ 1 - 1
netbox/dcim/tests/test_filtersets.py

@@ -5,7 +5,7 @@ from dcim.choices import *
 from dcim.filtersets import *
 from dcim.filtersets import *
 from dcim.models import *
 from dcim.models import *
 from ipam.models import ASN, IPAddress, RIR, VRF
 from ipam.models import ASN, IPAddress, RIR, VRF
-from netbox.choices import ColorChoices
+from netbox.choices import ColorChoices, WeightUnitChoices
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
 from users.models import User
 from users.models import User
 from utilities.testing import ChangeLoggedFilterSetTests, create_test_device
 from utilities.testing import ChangeLoggedFilterSetTests, create_test_device

+ 1 - 0
netbox/dcim/tests/test_models.py

@@ -6,6 +6,7 @@ from core.models import ObjectType
 from dcim.choices import *
 from dcim.choices import *
 from dcim.models import *
 from dcim.models import *
 from extras.models import CustomField
 from extras.models import CustomField
+from netbox.choices import WeightUnitChoices
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.data import drange
 from utilities.data import drange
 from virtualization.models import Cluster, ClusterType
 from virtualization.models import Cluster, ClusterType

+ 1 - 1
netbox/dcim/tests/test_views.py

@@ -10,7 +10,7 @@ from dcim.choices import *
 from dcim.constants import *
 from dcim.constants import *
 from dcim.models import *
 from dcim.models import *
 from ipam.models import ASN, RIR, VLAN, VRF
 from ipam.models import ASN, RIR, VLAN, VRF
-from netbox.choices import CSVDelimiterChoices, ImportFormatChoices
+from netbox.choices import CSVDelimiterChoices, ImportFormatChoices, WeightUnitChoices
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from users.models import User
 from users.models import User
 from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data
 from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data

+ 38 - 0
netbox/netbox/choices.py

@@ -7,8 +7,10 @@ __all__ = (
     'ButtonColorChoices',
     'ButtonColorChoices',
     'ColorChoices',
     'ColorChoices',
     'CSVDelimiterChoices',
     'CSVDelimiterChoices',
+    'DistanceUnitChoices',
     'ImportFormatChoices',
     'ImportFormatChoices',
     'ImportMethodChoices',
     'ImportMethodChoices',
+    'WeightUnitChoices',
 )
 )
 
 
 
 
@@ -157,3 +159,39 @@ class CSVDelimiterChoices(ChoiceSet):
         (SEMICOLON, _('Semicolon')),
         (SEMICOLON, _('Semicolon')),
         (TAB, _('Tab')),
         (TAB, _('Tab')),
     ]
     ]
+
+
+class DistanceUnitChoices(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')),
+    )
+
+
+class WeightUnitChoices(ChoiceSet):
+
+    # Metric
+    UNIT_KILOGRAM = 'kg'
+    UNIT_GRAM = 'g'
+
+    # Imperial
+    UNIT_POUND = 'lb'
+    UNIT_OUNCE = 'oz'
+
+    CHOICES = (
+        (UNIT_KILOGRAM, _('Kilograms')),
+        (UNIT_GRAM, _('Grams')),
+        (UNIT_POUND, _('Pounds')),
+        (UNIT_OUNCE, _('Ounces')),
+    )

+ 97 - 0
netbox/netbox/models/mixins.py

@@ -0,0 +1,97 @@
+from django.core.exceptions import ValidationError
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+from netbox.choices import *
+from utilities.conversion import to_grams, to_meters
+
+__all__ = (
+    'DistanceMixin',
+    'WeightMixin',
+)
+
+
+class WeightMixin(models.Model):
+    weight = models.DecimalField(
+        verbose_name=_('weight'),
+        max_digits=8,
+        decimal_places=2,
+        blank=True,
+        null=True
+    )
+    weight_unit = models.CharField(
+        verbose_name=_('weight unit'),
+        max_length=50,
+        choices=WeightUnitChoices,
+        blank=True,
+    )
+    # Stores the normalized weight (in grams) for database ordering
+    _abs_weight = models.PositiveBigIntegerField(
+        blank=True,
+        null=True
+    )
+
+    class Meta:
+        abstract = True
+
+    def save(self, *args, **kwargs):
+
+        # Store the given weight (if any) in grams for use in database ordering
+        if self.weight and self.weight_unit:
+            self._abs_weight = to_grams(self.weight, self.weight_unit)
+        else:
+            self._abs_weight = None
+
+        super().save(*args, **kwargs)
+
+    def clean(self):
+        super().clean()
+
+        # Validate weight and weight_unit
+        if self.weight and not self.weight_unit:
+            raise ValidationError(_("Must specify a unit when setting a weight"))
+
+
+class DistanceMixin(models.Model):
+    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=DistanceUnitChoices,
+        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
+    )
+
+    class Meta:
+        abstract = True
+
+    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 = ''
+
+        super().save(*args, **kwargs)
+
+    def clean(self):
+        super().clean()
+
+        # Validate distance and distance_unit
+        if self.distance and not self.distance_unit:
+            raise ValidationError(_("Must specify a unit when setting a distance"))

+ 14 - 0
netbox/netbox/tables/columns.py

@@ -35,6 +35,7 @@ __all__ = (
     'ContentTypesColumn',
     'ContentTypesColumn',
     'CustomFieldColumn',
     'CustomFieldColumn',
     'CustomLinkColumn',
     'CustomLinkColumn',
+    'DistanceColumn',
     'DurationColumn',
     'DurationColumn',
     'LinkedCountColumn',
     'LinkedCountColumn',
     'MarkdownColumn',
     'MarkdownColumn',
@@ -691,3 +692,16 @@ class ChoicesColumn(tables.Column):
             value.append(f'({omitted_count} more)')
             value.append(f'({omitted_count} more)')
 
 
         return ', '.join(value)
         return ', '.join(value)
+
+
+class DistanceColumn(TemplateColumn):
+    """
+    Distance with template code for formatting
+    """
+    template_code = """
+    {% load helpers %}
+    {% if record.distance %}{{ record.distance|floatformat:"-2" }} {{ record.distance_unit }}{% endif %}
+    """
+
+    def __init__(self, template_code=template_code, order_by='_abs_distance', **kwargs):
+        super().__init__(template_code=template_code, order_by=order_by, **kwargs)

+ 10 - 0
netbox/templates/circuits/circuit.html

@@ -34,6 +34,16 @@
             <th scope="row">{% trans "Status" %}</th>
             <th scope="row">{% trans "Status" %}</th>
             <td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
             <td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
           </tr>
           </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>
           <tr>
           <tr>
             <th scope="row">{% trans "Tenant" %}</th>
             <th scope="row">{% trans "Tenant" %}</th>
             <td>
             <td>

+ 2 - 1
netbox/utilities/conversion.py

@@ -2,7 +2,8 @@ from decimal import Decimal
 
 
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext as _
 
 
-from dcim.choices import CableLengthUnitChoices, WeightUnitChoices
+from dcim.choices import CableLengthUnitChoices
+from netbox.choices import WeightUnitChoices
 
 
 __all__ = (
 __all__ = (
     'to_grams',
     'to_grams',

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

@@ -4,6 +4,7 @@ from dcim.api.serializers_.device_components import InterfaceSerializer
 from dcim.choices import LinkStatusChoices
 from dcim.choices import LinkStatusChoices
 from netbox.api.fields import ChoiceField
 from netbox.api.fields import ChoiceField
 from netbox.api.serializers import NetBoxModelSerializer
 from netbox.api.serializers import NetBoxModelSerializer
+from netbox.choices import *
 from tenancy.api.serializers_.tenants import TenantSerializer
 from tenancy.api.serializers_.tenants import TenantSerializer
 from wireless.choices import *
 from wireless.choices import *
 from wireless.models import WirelessLink
 from wireless.models import WirelessLink
@@ -20,7 +21,7 @@ class WirelessLinkSerializer(NetBoxModelSerializer):
     tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     tenant = TenantSerializer(nested=True, required=False, allow_null=True)
     auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True)
     auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True)
     auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, 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)
+    distance_unit = ChoiceField(choices=DistanceUnitChoices, allow_blank=True, required=False, allow_null=True)
 
 
     class Meta:
     class Meta:
         model = WirelessLink
         model = WirelessLink

+ 0 - 18
netbox/wireless/choices.py

@@ -481,21 +481,3 @@ class WirelessAuthCipherChoices(ChoiceSet):
         (CIPHER_TKIP, 'TKIP'),
         (CIPHER_TKIP, 'TKIP'),
         (CIPHER_AES, 'AES'),
         (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')),
-    )

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

@@ -3,6 +3,7 @@ from django.utils.translation import gettext_lazy as _
 
 
 from dcim.choices import LinkStatusChoices
 from dcim.choices import LinkStatusChoices
 from ipam.models import VLAN
 from ipam.models import VLAN
+from netbox.choices import *
 from netbox.forms import NetBoxModelBulkEditForm
 from netbox.forms import NetBoxModelBulkEditForm
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms import add_blank_choice
 from utilities.forms import add_blank_choice
@@ -132,7 +133,7 @@ class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm):
     )
     )
     distance_unit = forms.ChoiceField(
     distance_unit = forms.ChoiceField(
         label=_('Distance unit'),
         label=_('Distance unit'),
-        choices=add_blank_choice(WirelessLinkDistanceUnitChoices),
+        choices=add_blank_choice(DistanceUnitChoices),
         required=False,
         required=False,
         initial=''
         initial=''
     )
     )

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

@@ -3,6 +3,7 @@ from django.utils.translation import gettext_lazy as _
 from dcim.choices import LinkStatusChoices
 from dcim.choices import LinkStatusChoices
 from dcim.models import Interface
 from dcim.models import Interface
 from ipam.models import VLAN
 from ipam.models import VLAN
+from netbox.choices import *
 from netbox.forms import NetBoxModelImportForm
 from netbox.forms import NetBoxModelImportForm
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField
 from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField
@@ -114,7 +115,7 @@ class WirelessLinkImportForm(NetBoxModelImportForm):
     )
     )
     distance_unit = CSVChoiceField(
     distance_unit = CSVChoiceField(
         label=_('Distance unit'),
         label=_('Distance unit'),
-        choices=WirelessLinkDistanceUnitChoices,
+        choices=DistanceUnitChoices,
         required=False,
         required=False,
         help_text=_('Distance unit')
         help_text=_('Distance unit')
     )
     )

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

@@ -2,6 +2,7 @@ from django import forms
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
 
 
 from dcim.choices import LinkStatusChoices
 from dcim.choices import LinkStatusChoices
+from netbox.choices import *
 from netbox.forms import NetBoxModelFilterSetForm
 from netbox.forms import NetBoxModelFilterSetForm
 from tenancy.forms import TenancyFilterForm
 from tenancy.forms import TenancyFilterForm
 from utilities.forms import add_blank_choice
 from utilities.forms import add_blank_choice
@@ -104,7 +105,7 @@ class WirelessLinkFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     )
     )
     distance_unit = forms.ChoiceField(
     distance_unit = forms.ChoiceField(
         label=_('Distance unit'),
         label=_('Distance unit'),
-        choices=add_blank_choice(WirelessLinkDistanceUnitChoices),
+        choices=add_blank_choice(DistanceUnitChoices),
         required=False
         required=False
     )
     )
     tag = TagFilterField(model)
     tag = TagFilterField(model)

+ 2 - 35
netbox/wireless/models.py

@@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _
 from dcim.choices import LinkStatusChoices
 from dcim.choices import LinkStatusChoices
 from dcim.constants import WIRELESS_IFACE_TYPES
 from dcim.constants import WIRELESS_IFACE_TYPES
 from netbox.models import NestedGroupModel, PrimaryModel
 from netbox.models import NestedGroupModel, PrimaryModel
+from netbox.models.mixins import DistanceMixin
 from utilities.conversion import to_meters
 from utilities.conversion import to_meters
 from .choices import *
 from .choices import *
 from .constants import *
 from .constants import *
@@ -126,7 +127,7 @@ def get_wireless_interface_types():
     return {'type__in': WIRELESS_IFACE_TYPES}
     return {'type__in': WIRELESS_IFACE_TYPES}
 
 
 
 
-class WirelessLink(WirelessAuthenticationBase, PrimaryModel):
+class WirelessLink(WirelessAuthenticationBase, DistanceMixin, PrimaryModel):
     """
     """
     A point-to-point connection between two wireless Interfaces.
     A point-to-point connection between two wireless Interfaces.
     """
     """
@@ -155,26 +156,6 @@ class WirelessLink(WirelessAuthenticationBase, PrimaryModel):
         choices=LinkStatusChoices,
         choices=LinkStatusChoices,
         default=LinkStatusChoices.STATUS_CONNECTED
         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(
     tenant = models.ForeignKey(
         to='tenancy.Tenant',
         to='tenancy.Tenant',
         on_delete=models.PROTECT,
         on_delete=models.PROTECT,
@@ -222,10 +203,6 @@ class WirelessLink(WirelessAuthenticationBase, PrimaryModel):
     def clean(self):
     def clean(self):
         super().clean()
         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
         # Validate interface types
         if self.interface_a.type not in WIRELESS_IFACE_TYPES:
         if self.interface_a.type not in WIRELESS_IFACE_TYPES:
             raise ValidationError({
             raise ValidationError({
@@ -241,16 +218,6 @@ class WirelessLink(WirelessAuthenticationBase, PrimaryModel):
             })
             })
 
 
     def save(self, *args, **kwargs):
     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
         # Store the parent Device for the A and B interfaces
         self._interface_a_device = self.interface_a.device
         self._interface_a_device = self.interface_a.device
         self._interface_b_device = self.interface_b.device
         self._interface_b_device = self.interface_b.device

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

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

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

@@ -4,7 +4,6 @@ import django_tables2 as tables
 from netbox.tables import NetBoxTable, columns
 from netbox.tables import NetBoxTable, columns
 from tenancy.tables import TenancyColumnsMixin
 from tenancy.tables import TenancyColumnsMixin
 from wireless.models import *
 from wireless.models import *
-from .template_code import WIRELESS_LINK_DISTANCE
 
 
 __all__ = (
 __all__ = (
     'WirelessLinkTable',
     'WirelessLinkTable',
@@ -37,10 +36,7 @@ class WirelessLinkTable(TenancyColumnsMixin, NetBoxTable):
         verbose_name=_('Interface B'),
         verbose_name=_('Interface B'),
         linkify=True
         linkify=True
     )
     )
-    distance = columns.TemplateColumn(
-        template_code=WIRELESS_LINK_DISTANCE,
-        order_by=('_abs_distance')
-    )
+    distance = columns.DistanceColumn()
     tags = columns.TagColumn(
     tags = columns.TagColumn(
         url_name='wireless:wirelesslink_list'
         url_name='wireless:wirelesslink_list'
     )
     )

+ 5 - 4
netbox/wireless/tests/test_filtersets.py

@@ -3,6 +3,7 @@ from django.test import TestCase
 from dcim.choices import InterfaceTypeChoices, LinkStatusChoices
 from dcim.choices import InterfaceTypeChoices, LinkStatusChoices
 from dcim.models import Interface
 from dcim.models import Interface
 from ipam.models import VLAN
 from ipam.models import VLAN
+from netbox.choices import DistanceUnitChoices
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from wireless.choices import *
 from wireless.choices import *
 from wireless.filtersets import *
 from wireless.filtersets import *
@@ -261,7 +262,7 @@ class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
             auth_psk='PSK1',
             auth_psk='PSK1',
             tenant=tenants[0],
             tenant=tenants[0],
             distance=10,
             distance=10,
-            distance_unit=WirelessLinkDistanceUnitChoices.UNIT_FOOT,
+            distance_unit=DistanceUnitChoices.UNIT_FOOT,
             description='foobar1'
             description='foobar1'
         ).save()
         ).save()
         WirelessLink(
         WirelessLink(
@@ -274,7 +275,7 @@ class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
             auth_psk='PSK2',
             auth_psk='PSK2',
             tenant=tenants[1],
             tenant=tenants[1],
             distance=20,
             distance=20,
-            distance_unit=WirelessLinkDistanceUnitChoices.UNIT_METER,
+            distance_unit=DistanceUnitChoices.UNIT_METER,
             description='foobar2'
             description='foobar2'
         ).save()
         ).save()
         WirelessLink(
         WirelessLink(
@@ -286,7 +287,7 @@ class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
             auth_cipher=WirelessAuthCipherChoices.CIPHER_AES,
             auth_cipher=WirelessAuthCipherChoices.CIPHER_AES,
             auth_psk='PSK3',
             auth_psk='PSK3',
             distance=30,
             distance=30,
-            distance_unit=WirelessLinkDistanceUnitChoices.UNIT_METER,
+            distance_unit=DistanceUnitChoices.UNIT_METER,
             tenant=tenants[2],
             tenant=tenants[2],
         ).save()
         ).save()
         WirelessLink(
         WirelessLink(
@@ -324,7 +325,7 @@ class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
     def test_distance_unit(self):
     def test_distance_unit(self):
-        params = {'distance_unit': WirelessLinkDistanceUnitChoices.UNIT_FOOT}
+        params = {'distance_unit': DistanceUnitChoices.UNIT_FOOT}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
 
     def test_description(self):
     def test_description(self):

+ 3 - 2
netbox/wireless/tests/test_views.py

@@ -2,6 +2,7 @@ from wireless.choices import *
 from wireless.models import *
 from wireless.models import *
 from dcim.choices import InterfaceTypeChoices, LinkStatusChoices
 from dcim.choices import InterfaceTypeChoices, LinkStatusChoices
 from dcim.models import Interface
 from dcim.models import Interface
+from netbox.choices import DistanceUnitChoices
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.testing import ViewTestCases, create_tags, create_test_device
 from utilities.testing import ViewTestCases, create_tags, create_test_device
 
 
@@ -161,7 +162,7 @@ class WirelessLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             'interface_b': interfaces[7].pk,
             'interface_b': interfaces[7].pk,
             'status': LinkStatusChoices.STATUS_PLANNED,
             'status': LinkStatusChoices.STATUS_PLANNED,
             'distance': 100,
             'distance': 100,
-            'distance_unit': WirelessLinkDistanceUnitChoices.UNIT_FOOT,
+            'distance_unit': DistanceUnitChoices.UNIT_FOOT,
             'tenant': tenants[1].pk,
             'tenant': tenants[1].pk,
             'tags': [t.pk for t in tags],
             'tags': [t.pk for t in tags],
         }
         }
@@ -183,5 +184,5 @@ class WirelessLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         cls.bulk_edit_data = {
         cls.bulk_edit_data = {
             'status': LinkStatusChoices.STATUS_PLANNED,
             'status': LinkStatusChoices.STATUS_PLANNED,
             'distance': 50,
             'distance': 50,
-            'distance_unit': WirelessLinkDistanceUnitChoices.UNIT_METER,
+            'distance_unit': DistanceUnitChoices.UNIT_METER,
         }
         }