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" !!! 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.

View File

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

View File

@ -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():

View File

@ -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 = (

View File

@ -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'
] ]

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -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__ = (

View File

@ -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',

View File

@ -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__ = (

View File

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

View File

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

View File

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

View File

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

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', '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)

View File

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

View File

@ -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',

View File

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

View File

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

View File

@ -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=''
) )

View File

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

View File

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

View File

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

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 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( distance = columns.DistanceColumn()
template_code=WIRELESS_LINK_DISTANCE,
order_by=('_abs_distance')
)
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='wireless:wirelesslink_list' url_name='wireless:wirelesslink_list'
) )

View File

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

View File

@ -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,
} }