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>
This commit is contained in:
Arthur Hanson 2024-10-10 11:37:33 -07:00 committed by GitHub
parent bc597c3c5d
commit 65687851fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 290 additions and 150 deletions

View File

@ -36,6 +36,12 @@ The operational status of the circuit. By default, the following statuses are av
!!! 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.
### 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
A brief description of the circuit.

View File

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

View File

@ -239,7 +239,7 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
class Meta:
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):
if not value.strip():

View File

@ -5,6 +5,7 @@ from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, C
from circuits.models import *
from dcim.models import Site
from ipam.models import ASN
from netbox.choices import DistanceUnitChoices
from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant
from utilities.forms import add_blank_choice
@ -160,6 +161,17 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
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(
label=_('Description'),
max_length=100,
@ -171,6 +183,7 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
fieldsets = (
FieldSet('provider', 'type', 'status', 'description', name=_('Circuit')),
FieldSet('provider_account', 'install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
FieldSet('distance', 'distance_unit', name=_('Attributes')),
FieldSet('tenant', name=_('Tenancy')),
)
nullable_fields = (

View File

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

View File

@ -5,8 +5,10 @@ from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, C
from circuits.models import *
from dcim.models import Region, Site, SiteGroup
from ipam.models import ASN
from netbox.choices import DistanceUnitChoices
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
from utilities.forms import add_blank_choice
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.rendering import FieldSet
from utilities.forms.widgets import DatePicker, NumberWithOptions
@ -114,7 +116,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
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('tenant_group_id', 'tenant_id', name=_('Tenant')),
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
@ -188,6 +190,15 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
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)

View File

@ -7,7 +7,7 @@ from ipam.models import ASN
from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm
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
__all__ = (
@ -108,7 +108,17 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
comments = CommentField()
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('tenant_group', 'tenant', name=_('Tenancy')),
)
@ -117,7 +127,7 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
model = Circuit
fields = [
'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 = {
'install_date': DatePicker(),

View File

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

View File

@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _
from circuits.choices import *
from dcim.models import CabledObjectModel
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 utilities.fields import ColorField
@ -34,7 +35,7 @@ class CircuitType(OrganizationalModel):
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
circuits. Each circuit is also assigned a CircuitType and a Site, and may optionally be assigned to a particular

View File

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

View File

@ -5,6 +5,7 @@ from circuits.filtersets import *
from circuits.models import *
from dcim.models import Cable, Region, Site, SiteGroup
from ipam.models import ASN, RIR
from netbox.choices import DistanceUnitChoices
from tenancy.models import Tenant, TenantGroup
from utilities.testing import ChangeLoggedFilterSetTests
@ -222,9 +223,9 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
ProviderNetwork.objects.bulk_create(provider_networks)
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[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),
@ -289,6 +290,14 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]}
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):
params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,56 +1,12 @@
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
from dcim.choices import *
from utilities.conversion import to_grams
__all__ = (
'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):
config_template = models.ForeignKey(
to='extras.ConfigTemplate',

View File

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

View File

@ -5,7 +5,7 @@ from dcim.choices import *
from dcim.filtersets import *
from dcim.models import *
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 users.models import User
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device

View File

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

View File

@ -10,7 +10,7 @@ from dcim.choices import *
from dcim.constants import *
from dcim.models import *
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 users.models import User
from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data

View File

@ -7,8 +7,10 @@ __all__ = (
'ButtonColorChoices',
'ColorChoices',
'CSVDelimiterChoices',
'DistanceUnitChoices',
'ImportFormatChoices',
'ImportMethodChoices',
'WeightUnitChoices',
)
@ -157,3 +159,39 @@ class CSVDelimiterChoices(ChoiceSet):
(SEMICOLON, _('Semicolon')),
(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')),
)

View File

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

View File

@ -35,6 +35,7 @@ __all__ = (
'ContentTypesColumn',
'CustomFieldColumn',
'CustomLinkColumn',
'DistanceColumn',
'DurationColumn',
'LinkedCountColumn',
'MarkdownColumn',
@ -691,3 +692,16 @@ class ChoicesColumn(tables.Column):
value.append(f'({omitted_count} more)')
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)

View File

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

View File

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

View File

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

View File

@ -481,21 +481,3 @@ 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')),
)

View File

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

View File

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

View File

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

View File

@ -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 netbox.models.mixins import DistanceMixin
from utilities.conversion import to_meters
from .choices import *
from .constants import *
@ -126,7 +127,7 @@ def get_wireless_interface_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.
"""
@ -155,26 +156,6 @@ 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,
@ -222,10 +203,6 @@ class WirelessLink(WirelessAuthenticationBase, PrimaryModel):
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:
raise ValidationError({
@ -241,16 +218,6 @@ 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
self._interface_b_device = self.interface_b.device

View File

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

View File

@ -4,7 +4,6 @@ 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',
@ -37,10 +36,7 @@ class WirelessLinkTable(TenancyColumnsMixin, NetBoxTable):
verbose_name=_('Interface B'),
linkify=True
)
distance = columns.TemplateColumn(
template_code=WIRELESS_LINK_DISTANCE,
order_by=('_abs_distance')
)
distance = columns.DistanceColumn()
tags = columns.TagColumn(
url_name='wireless:wirelesslink_list'
)

View File

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

View File

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