diff --git a/docs/models/circuits/circuit.md b/docs/models/circuits/circuit.md index 19fd8c88262..c75e2032212 100644 --- a/docs/models/circuits/circuit.md +++ b/docs/models/circuits/circuit.md @@ -36,6 +36,12 @@ The operational status of the circuit. By default, the following statuses are av !!! tip "Custom circuit statuses" Additional circuit statuses may be defined by setting `Circuit.status` under the [`FIELD_CHOICES`](../../configuration/data-validation.md#field_choices) configuration parameter. +### Distance + +!!! info "This field was introduced in NetBox v4.2." + +The distance between the circuit's two endpoints, including a unit designation (e.g. 100 meters or 25 feet). + ### Description A brief description of the circuit. diff --git a/netbox/circuits/api/serializers_/circuits.py b/netbox/circuits/api/serializers_/circuits.py index c605a4bd5c1..111fa6f8726 100644 --- a/netbox/circuits/api/serializers_/circuits.py +++ b/netbox/circuits/api/serializers_/circuits.py @@ -4,6 +4,7 @@ from dcim.api.serializers_.sites import SiteSerializer from netbox.api.fields import ChoiceField, RelatedObjectCountField from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer +from netbox.choices import DistanceUnitChoices from tenancy.api.serializers_.tenants import TenantSerializer from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer @@ -80,13 +81,14 @@ class CircuitSerializer(NetBoxModelSerializer): termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True) termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True) assignments = CircuitGroupAssignmentSerializer_(nested=True, many=True, required=False) + distance_unit = ChoiceField(choices=DistanceUnitChoices, allow_blank=True, required=False, allow_null=True) class Meta: model = Circuit fields = [ 'id', 'url', 'display_url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z', - 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'assignments', + 'distance', 'distance_unit', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'assignments', ] brief_fields = ('id', 'url', 'display', 'provider', 'cid', 'description') diff --git a/netbox/circuits/filtersets.py b/netbox/circuits/filtersets.py index c55807c7558..ebd1fe28d2f 100644 --- a/netbox/circuits/filtersets.py +++ b/netbox/circuits/filtersets.py @@ -239,7 +239,7 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte class Meta: model = Circuit - fields = ('id', 'cid', 'description', 'install_date', 'termination_date', 'commit_rate') + fields = ('id', 'cid', 'description', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit') def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py index 3bb50a8d01a..5cb7b5d306a 100644 --- a/netbox/circuits/forms/bulk_edit.py +++ b/netbox/circuits/forms/bulk_edit.py @@ -5,6 +5,7 @@ from circuits.models import * from dcim.models import Site from ipam.models import ASN +from netbox.choices import DistanceUnitChoices from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import add_blank_choice @@ -160,6 +161,17 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm): options=CircuitCommitRateChoices ) ) + distance = forms.DecimalField( + label=_('Distance'), + min_value=0, + required=False + ) + distance_unit = forms.ChoiceField( + label=_('Distance unit'), + choices=add_blank_choice(DistanceUnitChoices), + required=False, + initial='' + ) description = forms.CharField( label=_('Description'), max_length=100, @@ -171,6 +183,7 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm): fieldsets = ( FieldSet('provider', 'type', 'status', 'description', name=_('Circuit')), FieldSet('provider_account', 'install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')), + FieldSet('distance', 'distance_unit', name=_('Attributes')), FieldSet('tenant', name=_('Tenancy')), ) nullable_fields = ( diff --git a/netbox/circuits/forms/bulk_import.py b/netbox/circuits/forms/bulk_import.py index 1e7b6361a6e..851f548815d 100644 --- a/netbox/circuits/forms/bulk_import.py +++ b/netbox/circuits/forms/bulk_import.py @@ -5,6 +5,7 @@ from circuits.choices import * from circuits.models import * from dcim.models import Site +from netbox.choices import DistanceUnitChoices from netbox.forms import NetBoxModelImportForm from tenancy.models import Tenant from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField @@ -95,6 +96,12 @@ class CircuitImportForm(NetBoxModelImportForm): choices=CircuitStatusChoices, help_text=_('Operational status') ) + distance_unit = CSVChoiceField( + label=_('Distance unit'), + choices=DistanceUnitChoices, + required=False, + help_text=_('Distance unit') + ) tenant = CSVModelChoiceField( label=_('Tenant'), queryset=Tenant.objects.all(), @@ -107,7 +114,7 @@ class Meta: model = Circuit fields = [ 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date', - 'commit_rate', 'description', 'comments', 'tags' + 'commit_rate', 'distance', 'distance_unit', 'description', 'comments', 'tags' ] diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py index a658dd641e6..2e9b358e891 100644 --- a/netbox/circuits/forms/filtersets.py +++ b/netbox/circuits/forms/filtersets.py @@ -5,8 +5,10 @@ from circuits.models import * from dcim.models import Region, Site, SiteGroup from ipam.models import ASN +from netbox.choices import DistanceUnitChoices from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import TenancyFilterForm, ContactModelFilterForm +from utilities.forms import add_blank_choice from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField from utilities.forms.rendering import FieldSet from utilities.forms.widgets import DatePicker, NumberWithOptions @@ -114,7 +116,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi fieldsets = ( FieldSet('q', 'filter_id', 'tag'), FieldSet('provider_id', 'provider_account_id', 'provider_network_id', name=_('Provider')), - FieldSet('type_id', 'status', 'install_date', 'termination_date', 'commit_rate', name=_('Attributes')), + FieldSet('type_id', 'status', 'install_date', 'termination_date', 'commit_rate', 'distance', 'distance_unit', name=_('Attributes')), FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')), FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')), FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')), @@ -188,6 +190,15 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi options=CircuitCommitRateChoices ) ) + distance = forms.DecimalField( + label=_('Distance'), + required=False, + ) + distance_unit = forms.ChoiceField( + label=_('Distance unit'), + choices=add_blank_choice(DistanceUnitChoices), + required=False + ) tag = TagFilterField(model) diff --git a/netbox/circuits/forms/model_forms.py b/netbox/circuits/forms/model_forms.py index 9a54fdccb91..e00034a1098 100644 --- a/netbox/circuits/forms/model_forms.py +++ b/netbox/circuits/forms/model_forms.py @@ -7,7 +7,7 @@ from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField -from utilities.forms.rendering import FieldSet, TabbedGroups +from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups from utilities.forms.widgets import DatePicker, NumberWithOptions __all__ = ( @@ -108,7 +108,17 @@ class CircuitForm(TenancyForm, NetBoxModelForm): comments = CommentField() fieldsets = ( - FieldSet('provider', 'provider_account', 'cid', 'type', 'status', 'description', 'tags', name=_('Circuit')), + FieldSet( + 'provider', + 'provider_account', + 'cid', + 'type', + 'status', + InlineFields('distance', 'distance_unit', label=_('Distance')), + 'description', + 'tags', + name=_('Circuit') + ), FieldSet('install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')), FieldSet('tenant_group', 'tenant', name=_('Tenancy')), ) @@ -117,7 +127,7 @@ class Meta: model = Circuit fields = [ 'cid', 'type', 'provider', 'provider_account', 'status', 'install_date', 'termination_date', 'commit_rate', - 'description', 'tenant_group', 'tenant', 'comments', 'tags', + 'distance', 'distance_unit', 'description', 'tenant_group', 'tenant', 'comments', 'tags', ] widgets = { 'install_date': DatePicker(), diff --git a/netbox/circuits/migrations/0045_circuit_distance.py b/netbox/circuits/migrations/0045_circuit_distance.py new file mode 100644 index 00000000000..6c970339d73 --- /dev/null +++ b/netbox/circuits/migrations/0045_circuit_distance.py @@ -0,0 +1,28 @@ +# Generated by Django 5.0.9 on 2024-09-26 22:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('circuits', '0044_circuit_groups'), + ] + + operations = [ + migrations.AddField( + model_name='circuit', + name='_abs_distance', + field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True), + ), + migrations.AddField( + model_name='circuit', + name='distance', + field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True), + ), + migrations.AddField( + model_name='circuit', + name='distance_unit', + field=models.CharField(blank=True, max_length=50), + ), + ] diff --git a/netbox/circuits/models/circuits.py b/netbox/circuits/models/circuits.py index 68c938aa99a..7df7543b385 100644 --- a/netbox/circuits/models/circuits.py +++ b/netbox/circuits/models/circuits.py @@ -6,6 +6,7 @@ from circuits.choices import * from dcim.models import CabledObjectModel from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel +from netbox.models.mixins import DistanceMixin from netbox.models.features import ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, ImageAttachmentsMixin, TagsMixin from utilities.fields import ColorField @@ -37,7 +38,7 @@ class Meta: verbose_name_plural = _('circuit types') -class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel): +class Circuit(ContactsMixin, ImageAttachmentsMixin, DistanceMixin, PrimaryModel): """ A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple circuits. Each circuit is also assigned a CircuitType and a Site, and may optionally be assigned to a particular diff --git a/netbox/circuits/tables/circuits.py b/netbox/circuits/tables/circuits.py index eefe2bd22d4..e79212a1461 100644 --- a/netbox/circuits/tables/circuits.py +++ b/netbox/circuits/tables/circuits.py @@ -76,6 +76,7 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): commit_rate = CommitRateColumn( verbose_name=_('Commit Rate') ) + distance = columns.DistanceColumn() comments = columns.MarkdownColumn( verbose_name=_('Comments') ) diff --git a/netbox/circuits/tests/test_filtersets.py b/netbox/circuits/tests/test_filtersets.py index bb350f0d12c..93958298c6f 100644 --- a/netbox/circuits/tests/test_filtersets.py +++ b/netbox/circuits/tests/test_filtersets.py @@ -5,6 +5,7 @@ from circuits.models import * from dcim.models import Cable, Region, Site, SiteGroup from ipam.models import ASN, RIR +from netbox.choices import DistanceUnitChoices from tenancy.models import Tenant, TenantGroup from utilities.testing import ChangeLoggedFilterSetTests @@ -222,9 +223,9 @@ def setUpTestData(cls): ProviderNetwork.objects.bulk_create(provider_networks) circuits = ( - Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', termination_date='2021-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar1'), - Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', termination_date='2021-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar2'), - Circuit(provider=providers[0], provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', termination_date='2021-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED), + Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', termination_date='2021-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar1', distance=10, distance_unit=DistanceUnitChoices.UNIT_FOOT), + Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', termination_date='2021-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar2', distance=20, distance_unit=DistanceUnitChoices.UNIT_METER), + Circuit(provider=providers[0], provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', termination_date='2021-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED, distance=30, distance_unit=DistanceUnitChoices.UNIT_METER), Circuit(provider=providers[1], provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[1], cid='Test Circuit 4', install_date='2020-01-04', termination_date='2021-01-04', commit_rate=4000, status=CircuitStatusChoices.STATUS_PLANNED), Circuit(provider=providers[1], provider_account=provider_accounts[2], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 5', install_date='2020-01-05', termination_date='2021-01-05', commit_rate=5000, status=CircuitStatusChoices.STATUS_OFFLINE), Circuit(provider=providers[1], provider_account=provider_accounts[2], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 6', install_date='2020-01-06', termination_date='2021-01-06', commit_rate=6000, status=CircuitStatusChoices.STATUS_OFFLINE), @@ -289,6 +290,14 @@ def test_status(self): params = {'status': [CircuitStatusChoices.STATUS_ACTIVE, CircuitStatusChoices.STATUS_PLANNED]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) + def test_distance(self): + params = {'distance': [10, 20]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + + def test_distance_unit(self): + params = {'distance_unit': DistanceUnitChoices.UNIT_FOOT} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + def test_description(self): params = {'description': ['foobar1', 'foobar2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/dcim/api/serializers_/devicetypes.py b/netbox/dcim/api/serializers_/devicetypes.py index cda73886285..0ce2af2f89a 100644 --- a/netbox/dcim/api/serializers_/devicetypes.py +++ b/netbox/dcim/api/serializers_/devicetypes.py @@ -7,6 +7,7 @@ from dcim.models import DeviceType, ModuleType from netbox.api.fields import ChoiceField, RelatedObjectCountField from netbox.api.serializers import NetBoxModelSerializer +from netbox.choices import * from .manufacturers import ManufacturerSerializer from .platforms import PlatformSerializer diff --git a/netbox/dcim/api/serializers_/racks.py b/netbox/dcim/api/serializers_/racks.py index ee76b09ce06..1378c265a85 100644 --- a/netbox/dcim/api/serializers_/racks.py +++ b/netbox/dcim/api/serializers_/racks.py @@ -6,6 +6,7 @@ from dcim.models import Rack, RackReservation, RackRole, RackType from netbox.api.fields import ChoiceField, RelatedObjectCountField from netbox.api.serializers import NetBoxModelSerializer +from netbox.choices import * from netbox.config import ConfigItem from tenancy.api.serializers_.tenants import TenantSerializer from users.api.serializers_.users import UserSerializer diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 848f57d7e61..d530db42d22 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -1546,24 +1546,6 @@ class CableLengthUnitChoices(ChoiceSet): ) -class WeightUnitChoices(ChoiceSet): - - # Metric - UNIT_KILOGRAM = 'kg' - UNIT_GRAM = 'g' - - # Imperial - UNIT_POUND = 'lb' - UNIT_OUNCE = 'oz' - - CHOICES = ( - (UNIT_KILOGRAM, _('Kilograms')), - (UNIT_GRAM, _('Grams')), - (UNIT_POUND, _('Pounds')), - (UNIT_OUNCE, _('Ounces')), - ) - - # # CableTerminations # diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 96036f4da1e..4d3a7acb191 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -8,6 +8,7 @@ from dcim.models import * from extras.models import ConfigTemplate from ipam.models import ASN, VLAN, VLANGroup, VRF +from netbox.choices import * from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from users.models import User diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index e9c8b362e52..551ec73a28f 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -10,6 +10,7 @@ from dcim.models import * from extras.models import ConfigTemplate from ipam.models import VRF, IPAddress +from netbox.choices import * from netbox.forms import NetBoxModelImportForm from tenancy.models import Tenant from utilities.forms.fields import ( diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index e2b6fda07a7..5a1a43a799d 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -7,6 +7,7 @@ from extras.forms import LocalConfigContextFilterForm from extras.models import ConfigTemplate from ipam.models import ASN, VRF +from netbox.choices import * from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import ContactModelFilterForm, TenancyFilterForm from users.models import User diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index a2b78957e40..8af43e2a52c 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -21,11 +21,12 @@ from netbox.choices import ColorChoices from netbox.config import ConfigItem from netbox.models import OrganizationalModel, PrimaryModel +from netbox.models.mixins import WeightMixin from netbox.models.features import ContactsMixin, ImageAttachmentsMixin from utilities.fields import ColorField, CounterCacheField, NaturalOrderingField from utilities.tracking import TrackingModelMixin from .device_components import * -from .mixins import RenderConfigMixin, WeightMixin +from .mixins import RenderConfigMixin __all__ = ( diff --git a/netbox/dcim/models/mixins.py b/netbox/dcim/models/mixins.py index d4a05699cac..8dbcb35b7c2 100644 --- a/netbox/dcim/models/mixins.py +++ b/netbox/dcim/models/mixins.py @@ -1,56 +1,12 @@ from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import gettext_lazy as _ -from dcim.choices import * -from utilities.conversion import to_grams __all__ = ( 'RenderConfigMixin', - 'WeightMixin', ) -class WeightMixin(models.Model): - weight = models.DecimalField( - verbose_name=_('weight'), - max_digits=8, - decimal_places=2, - blank=True, - null=True - ) - weight_unit = models.CharField( - verbose_name=_('weight unit'), - max_length=50, - choices=WeightUnitChoices, - blank=True, - ) - # Stores the normalized weight (in grams) for database ordering - _abs_weight = models.PositiveBigIntegerField( - blank=True, - null=True - ) - - class Meta: - abstract = True - - def save(self, *args, **kwargs): - - # Store the given weight (if any) in grams for use in database ordering - if self.weight and self.weight_unit: - self._abs_weight = to_grams(self.weight, self.weight_unit) - else: - self._abs_weight = None - - super().save(*args, **kwargs) - - def clean(self): - super().clean() - - # Validate weight and weight_unit - if self.weight and not self.weight_unit: - raise ValidationError(_("Must specify a unit when setting a weight")) - - class RenderConfigMixin(models.Model): config_template = models.ForeignKey( to='extras.ConfigTemplate', diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index 3aead09cac3..fe2e9560db2 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -16,13 +16,13 @@ from dcim.svg import RackElevationSVG from netbox.choices import ColorChoices from netbox.models import OrganizationalModel, PrimaryModel +from netbox.models.mixins import WeightMixin from netbox.models.features import ContactsMixin, ImageAttachmentsMixin from utilities.conversion import to_grams from utilities.data import array_to_string, drange from utilities.fields import ColorField, NaturalOrderingField from .device_components import PowerPort from .devices import Device, Module -from .mixins import WeightMixin from .power import PowerFeed __all__ = ( diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index afb360d76a9..f0b1454fb12 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -5,7 +5,7 @@ from dcim.filtersets import * from dcim.models import * from ipam.models import ASN, IPAddress, RIR, VRF -from netbox.choices import ColorChoices +from netbox.choices import ColorChoices, WeightUnitChoices from tenancy.models import Tenant, TenantGroup from users.models import User from utilities.testing import ChangeLoggedFilterSetTests, create_test_device diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 1c3dbb90b93..f183eccb32a 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -6,6 +6,7 @@ from dcim.choices import * from dcim.models import * from extras.models import CustomField +from netbox.choices import WeightUnitChoices from tenancy.models import Tenant from utilities.data import drange from virtualization.models import Cluster, ClusterType diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 7d6c3433735..18a8da4454a 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -10,7 +10,7 @@ from dcim.constants import * from dcim.models import * from ipam.models import ASN, RIR, VLAN, VRF -from netbox.choices import CSVDelimiterChoices, ImportFormatChoices +from netbox.choices import CSVDelimiterChoices, ImportFormatChoices, WeightUnitChoices from tenancy.models import Tenant from users.models import User from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data diff --git a/netbox/netbox/choices.py b/netbox/netbox/choices.py index 4fd7302556e..5c3110745ff 100644 --- a/netbox/netbox/choices.py +++ b/netbox/netbox/choices.py @@ -7,8 +7,10 @@ 'ButtonColorChoices', 'ColorChoices', 'CSVDelimiterChoices', + 'DistanceUnitChoices', 'ImportFormatChoices', 'ImportMethodChoices', + 'WeightUnitChoices', ) @@ -157,3 +159,39 @@ class CSVDelimiterChoices(ChoiceSet): (SEMICOLON, _('Semicolon')), (TAB, _('Tab')), ] + + +class DistanceUnitChoices(ChoiceSet): + + # Metric + UNIT_KILOMETER = 'km' + UNIT_METER = 'm' + + # Imperial + UNIT_MILE = 'mi' + UNIT_FOOT = 'ft' + + CHOICES = ( + (UNIT_KILOMETER, _('Kilometers')), + (UNIT_METER, _('Meters')), + (UNIT_MILE, _('Miles')), + (UNIT_FOOT, _('Feet')), + ) + + +class WeightUnitChoices(ChoiceSet): + + # Metric + UNIT_KILOGRAM = 'kg' + UNIT_GRAM = 'g' + + # Imperial + UNIT_POUND = 'lb' + UNIT_OUNCE = 'oz' + + CHOICES = ( + (UNIT_KILOGRAM, _('Kilograms')), + (UNIT_GRAM, _('Grams')), + (UNIT_POUND, _('Pounds')), + (UNIT_OUNCE, _('Ounces')), + ) diff --git a/netbox/netbox/models/mixins.py b/netbox/netbox/models/mixins.py new file mode 100644 index 00000000000..804e0b71ada --- /dev/null +++ b/netbox/netbox/models/mixins.py @@ -0,0 +1,97 @@ +from django.core.exceptions import ValidationError +from django.db import models +from django.utils.translation import gettext_lazy as _ +from netbox.choices import * +from utilities.conversion import to_grams, to_meters + +__all__ = ( + 'DistanceMixin', + 'WeightMixin', +) + + +class WeightMixin(models.Model): + weight = models.DecimalField( + verbose_name=_('weight'), + max_digits=8, + decimal_places=2, + blank=True, + null=True + ) + weight_unit = models.CharField( + verbose_name=_('weight unit'), + max_length=50, + choices=WeightUnitChoices, + blank=True, + ) + # Stores the normalized weight (in grams) for database ordering + _abs_weight = models.PositiveBigIntegerField( + blank=True, + null=True + ) + + class Meta: + abstract = True + + def save(self, *args, **kwargs): + + # Store the given weight (if any) in grams for use in database ordering + if self.weight and self.weight_unit: + self._abs_weight = to_grams(self.weight, self.weight_unit) + else: + self._abs_weight = None + + super().save(*args, **kwargs) + + def clean(self): + super().clean() + + # Validate weight and weight_unit + if self.weight and not self.weight_unit: + raise ValidationError(_("Must specify a unit when setting a weight")) + + +class DistanceMixin(models.Model): + distance = models.DecimalField( + verbose_name=_('distance'), + max_digits=8, + decimal_places=2, + blank=True, + null=True + ) + distance_unit = models.CharField( + verbose_name=_('distance unit'), + max_length=50, + choices=DistanceUnitChoices, + blank=True, + ) + # Stores the normalized distance (in meters) for database ordering + _abs_distance = models.DecimalField( + max_digits=10, + decimal_places=4, + blank=True, + null=True + ) + + class Meta: + abstract = True + + def save(self, *args, **kwargs): + # Store the given distance (if any) in meters for use in database ordering + if self.distance is not None and self.distance_unit: + self._abs_distance = to_meters(self.distance, self.distance_unit) + else: + self._abs_distance = None + + # Clear distance_unit if no distance is defined + if self.distance is None: + self.distance_unit = '' + + super().save(*args, **kwargs) + + def clean(self): + super().clean() + + # Validate distance and distance_unit + if self.distance and not self.distance_unit: + raise ValidationError(_("Must specify a unit when setting a distance")) diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index 0e70a1624f7..ee142039669 100644 --- a/netbox/netbox/tables/columns.py +++ b/netbox/netbox/tables/columns.py @@ -35,6 +35,7 @@ 'ContentTypesColumn', 'CustomFieldColumn', 'CustomLinkColumn', + 'DistanceColumn', 'DurationColumn', 'LinkedCountColumn', 'MarkdownColumn', @@ -691,3 +692,16 @@ def render(self, value): value.append(f'({omitted_count} more)') return ', '.join(value) + + +class DistanceColumn(TemplateColumn): + """ + Distance with template code for formatting + """ + template_code = """ + {% load helpers %} + {% if record.distance %}{{ record.distance|floatformat:"-2" }} {{ record.distance_unit }}{% endif %} + """ + + def __init__(self, template_code=template_code, order_by='_abs_distance', **kwargs): + super().__init__(template_code=template_code, order_by=order_by, **kwargs) diff --git a/netbox/templates/circuits/circuit.html b/netbox/templates/circuits/circuit.html index 5c2442b90f6..52c7d98949b 100644 --- a/netbox/templates/circuits/circuit.html +++ b/netbox/templates/circuits/circuit.html @@ -34,6 +34,16 @@