mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
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:
parent
bc597c3c5d
commit
65687851fe
@ -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.
|
||||||
|
@ -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')
|
||||||
|
|
||||||
|
@ -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():
|
||||||
|
@ -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 = (
|
||||||
|
@ -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'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
netbox/circuits/migrations/0045_circuit_distance.py
Normal file
28
netbox/circuits/migrations/0045_circuit_distance.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
@ -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
|
||||||
|
@ -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')
|
||||||
)
|
)
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
#
|
#
|
||||||
|
@ -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
|
||||||
|
@ -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 (
|
||||||
|
@ -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
|
||||||
|
@ -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__ = (
|
||||||
|
@ -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',
|
||||||
|
@ -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__ = (
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
netbox/netbox/models/mixins.py
Normal file
97
netbox/netbox/models/mixins.py
Normal 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"))
|
@ -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)
|
||||||
|
@ -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,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',
|
||||||
|
@ -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
|
||||||
|
@ -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')),
|
|
||||||
)
|
|
||||||
|
@ -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=''
|
||||||
)
|
)
|
||||||
|
@ -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,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)
|
||||||
|
@ -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
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
WIRELESS_LINK_DISTANCE = """
|
|
||||||
{% load helpers %}
|
|
||||||
{% if record.distance %}{{ record.distance|floatformat:"-2" }} {{ record.distance_unit }}{% endif %}
|
|
||||||
"""
|
|
@ -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'
|
||||||
)
|
)
|
||||||
|
@ -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):
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user