18417 Add outer_height to racks (#18940)

* 18417 add rack outer height

* 18417 add rack outer height

* 18417 fix tests

* 18417 fix validation message

* Update netbox/dcim/filtersets.py

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>

* Update netbox/dcim/filtersets.py

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>

* Update netbox/dcim/models/racks.py

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>

* Update netbox/dcim/models/racks.py

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>

* Update netbox/dcim/models/racks.py

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>

* Update netbox/dcim/models/racks.py

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>

* 16224 review changes

* 16224 review changes

* 16224 update table display

* 18417 use TemplateColumn

* 18417 review changes

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
This commit is contained in:
Arthur Hanson
2025-03-26 05:42:13 -07:00
committed by GitHub
parent fe7cc8cae9
commit 7a71c7b8f8
12 changed files with 128 additions and 49 deletions

View File

@@ -70,8 +70,8 @@ class RackTypeSerializer(RackBaseSerializer):
model = RackType
fields = [
'id', 'url', 'display_url', 'display', 'manufacturer', 'model', 'slug', 'description', 'form_factor',
'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'weight',
'max_weight', 'weight_unit', 'mounting_depth', 'description', 'comments', 'tags',
'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_height', 'outer_depth',
'outer_unit', 'weight', 'max_weight', 'weight_unit', 'mounting_depth', 'description', 'comments', 'tags',
'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description')
@@ -129,9 +129,9 @@ class RackSerializer(RackBaseSerializer):
fields = [
'id', 'url', 'display_url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status',
'role', 'serial', 'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'weight',
'max_weight', 'weight_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth',
'airflow', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
'powerfeed_count',
'max_weight', 'weight_unit', 'desc_units', 'outer_width', 'outer_height', 'outer_depth', 'outer_unit',
'mounting_depth', 'airflow', 'description', 'comments', 'tags', 'custom_fields',
'created', 'last_updated', 'device_count', 'powerfeed_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count')

View File

@@ -313,8 +313,8 @@ class RackTypeFilterSet(NetBoxModelFilterSet):
class Meta:
model = RackType
fields = (
'id', 'model', 'slug', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth',
'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description',
'id', 'model', 'slug', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_height',
'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description',
)
def search(self, queryset, name, value):
@@ -426,8 +426,8 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
model = Rack
fields = (
'id', 'name', 'facility_id', 'asset_tag', 'u_height', 'starting_unit', 'desc_units', 'outer_width',
'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit',
'description',
'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight',
'weight_unit', 'description',
)
def search(self, queryset, name, value):

View File

@@ -260,6 +260,11 @@ class RackTypeBulkEditForm(NetBoxModelBulkEditForm):
required=False,
min_value=1
)
outer_height = forms.IntegerField(
label=_('Outer height'),
required=False,
min_value=1
)
outer_depth = forms.IntegerField(
label=_('Outer depth'),
required=False,
@@ -302,7 +307,7 @@ class RackTypeBulkEditForm(NetBoxModelBulkEditForm):
fieldsets = (
FieldSet('manufacturer', 'description', 'form_factor', 'width', 'u_height', name=_('Rack Type')),
FieldSet(
InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
InlineFields('outer_width', 'outer_height', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
'mounting_depth',
name=_('Dimensions')
@@ -310,7 +315,7 @@ class RackTypeBulkEditForm(NetBoxModelBulkEditForm):
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
)
nullable_fields = (
'outer_width', 'outer_depth', 'outer_unit', 'weight',
'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'weight',
'max_weight', 'weight_unit', 'description', 'comments',
)
@@ -404,6 +409,11 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
required=False,
min_value=1
)
outer_height = forms.IntegerField(
label=_('Outer height'),
required=False,
min_value=1
)
outer_depth = forms.IntegerField(
label=_('Outer depth'),
required=False,
@@ -451,15 +461,13 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
fieldsets = (
FieldSet('status', 'role', 'tenant', 'serial', 'asset_tag', 'rack_type', 'description', name=_('Rack')),
FieldSet('region', 'site_group', 'site', 'location', name=_('Location')),
FieldSet(
'form_factor', 'width', 'u_height', 'desc_units', 'airflow', 'outer_width', 'outer_depth', 'outer_unit',
'mounting_depth', name=_('Hardware')
),
FieldSet('outer_width', 'outer_height', 'outer_depth', 'outer_unit', name=_('Outer Dimensions')),
FieldSet('form_factor', 'width', 'u_height', 'desc_units', 'airflow', 'mounting_depth', name=_('Hardware')),
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
)
nullable_fields = (
'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'weight',
'max_weight', 'weight_unit', 'description', 'comments',
'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_height', 'outer_depth',
'outer_unit', 'weight', 'max_weight', 'weight_unit', 'description', 'comments',
)

View File

@@ -222,7 +222,7 @@ class RackTypeImportForm(NetBoxModelImportForm):
model = RackType
fields = (
'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight',
'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight',
'weight_unit', 'description', 'comments', 'tags',
)
@@ -307,7 +307,7 @@ class RackImportForm(NetBoxModelImportForm):
model = Rack
fields = (
'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'rack_type', 'form_factor', 'serial',
'asset_tag', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
'asset_tag', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_height', 'outer_depth', 'outer_unit',
'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
)

View File

@@ -226,7 +226,7 @@ class RackTypeForm(NetBoxModelForm):
FieldSet('manufacturer', 'model', 'slug', 'description', 'form_factor', 'tags', name=_('Rack Type')),
FieldSet(
'width', 'u_height',
InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
InlineFields('outer_width', 'outer_height', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
'mounting_depth', name=_('Dimensions')
),
@@ -237,8 +237,8 @@ class RackTypeForm(NetBoxModelForm):
model = RackType
fields = [
'manufacturer', 'model', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
'description', 'comments', 'tags',
'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight',
'weight_unit', 'description', 'comments', 'tags',
]
@@ -283,8 +283,8 @@ class RackForm(TenancyForm, NetBoxModelForm):
fields = [
'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width',
'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit',
'description', 'comments', 'tags',
'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow', 'weight', 'max_weight',
'weight_unit', 'description', 'comments', 'tags',
]
def __init__(self, *args, **kwargs):
@@ -306,7 +306,8 @@ class RackForm(TenancyForm, NetBoxModelForm):
*self.fieldsets,
FieldSet(
'form_factor', 'width', 'starting_unit', 'u_height',
InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
InlineFields('outer_width', 'outer_height', 'outer_depth', 'outer_unit',
label=_('Outer Dimensions')),
InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
'mounting_depth', 'desc_units', name=_('Dimensions')
),

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.2b1 on 2025-03-18 15:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0202_location_comments_region_comments_sitegroup_comments'),
]
operations = [
migrations.AddField(
model_name='rack',
name='outer_height',
field=models.PositiveSmallIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='racktype',
name='outer_height',
field=models.PositiveSmallIntegerField(blank=True, null=True),
),
]

View File

@@ -73,6 +73,12 @@ class RackBase(WeightMixin, PrimaryModel):
null=True,
help_text=_('Outer dimension of rack (width)')
)
outer_height = models.PositiveSmallIntegerField(
verbose_name=_('outer height'),
blank=True,
null=True,
help_text=_('Outer dimension of rack (height)')
)
outer_depth = models.PositiveSmallIntegerField(
verbose_name=_('outer depth'),
blank=True,
@@ -140,7 +146,7 @@ class RackType(RackBase):
)
clone_fields = (
'manufacturer', 'form_factor', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
'manufacturer', 'form_factor', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_height', 'outer_depth',
'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
)
prerequisite_models = (
@@ -173,8 +179,8 @@ class RackType(RackBase):
super().clean()
# Validate outer dimensions and unit
if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_unit:
raise ValidationError(_("Must specify a unit when setting an outer width/depth"))
if any([self.outer_width, self.outer_depth, self.outer_height]) and not self.outer_unit:
raise ValidationError(_("Must specify a unit when setting an outer dimension"))
# Validate max_weight and weight_unit
if self.max_weight and not self.weight_unit:
@@ -188,7 +194,7 @@ class RackType(RackBase):
self._abs_max_weight = None
# Clear unit if outer width & depth are not set
if self.outer_width is None and self.outer_depth is None:
if not any([self.outer_width, self.outer_depth, self.outer_height]):
self.outer_unit = None
super().save(*args, **kwargs)
@@ -235,8 +241,8 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
"""
# Fields which cannot be set locally if a RackType is assigned
RACKTYPE_FIELDS = (
'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth',
'outer_unit', 'mounting_depth', 'weight', 'weight_unit', 'max_weight',
'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_height',
'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'weight_unit', 'max_weight',
)
form_factor = models.CharField(
@@ -329,7 +335,8 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
clone_fields = (
'site', 'location', 'tenant', 'status', 'role', 'form_factor', 'width', 'airflow', 'u_height', 'desc_units',
'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
'outer_width', 'outer_height', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight',
'weight_unit',
)
prerequisite_models = (
'dcim.Site',
@@ -364,8 +371,8 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
raise ValidationError(_("Assigned location must belong to parent site ({site}).").format(site=self.site))
# Validate outer dimensions and unit
if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_unit:
raise ValidationError(_("Must specify a unit when setting an outer width/depth"))
if any([self.outer_width, self.outer_depth, self.outer_height]) and not self.outer_unit:
raise ValidationError(_("Must specify a unit when setting an outer dimension"))
# Validate max_weight and weight_unit
if self.max_weight and not self.weight_unit:
@@ -414,7 +421,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
self._abs_max_weight = None
# Clear unit if outer width & depth are not set
if self.outer_width is None and self.outer_depth is None:
if not any([self.outer_width, self.outer_depth, self.outer_height]):
self.outer_unit = None
super().save(*args, **kwargs)

View File

@@ -5,7 +5,7 @@ from django_tables2.utils import Accessor
from dcim.models import Rack, RackReservation, RackRole, RackType
from netbox.tables import NetBoxTable, columns
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from .template_code import WEIGHT
from .template_code import OUTER_UNIT, WEIGHT
__all__ = (
'RackTable',
@@ -62,12 +62,16 @@ class RackTypeTable(NetBoxTable):
template_code="{{ value }}U",
verbose_name=_('Height')
)
outer_width = tables.TemplateColumn(
template_code="{{ record.outer_width }} {{ record.outer_unit }}",
outer_width = columns.TemplateColumn(
template_code=OUTER_UNIT,
verbose_name=_('Outer Width')
)
outer_depth = tables.TemplateColumn(
template_code="{{ record.outer_depth }} {{ record.outer_unit }}",
outer_height = columns.TemplateColumn(
template_code=OUTER_UNIT,
verbose_name=_('Outer Height')
)
outer_depth = columns.TemplateColumn(
template_code=OUTER_UNIT,
verbose_name=_('Outer Depth')
)
weight = columns.TemplateColumn(
@@ -96,8 +100,8 @@ class RackTypeTable(NetBoxTable):
model = RackType
fields = (
'pk', 'id', 'model', 'manufacturer', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width',
'outer_depth', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'description', 'comments',
'instance_count', 'tags', 'created', 'last_updated',
'outer_height', 'outer_depth', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'description',
'comments', 'instance_count', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'model', 'manufacturer', 'type', 'u_height', 'description', 'instance_count',
@@ -159,12 +163,16 @@ class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
tags = columns.TagColumn(
url_name='dcim:rack_list'
)
outer_width = tables.TemplateColumn(
template_code="{{ record.outer_width }} {{ record.outer_unit }}",
outer_width = columns.TemplateColumn(
template_code=OUTER_UNIT,
verbose_name=_('Outer Width')
)
outer_depth = tables.TemplateColumn(
template_code="{{ record.outer_depth }} {{ record.outer_unit }}",
outer_height = columns.TemplateColumn(
template_code=OUTER_UNIT,
verbose_name=_('Outer Height')
)
outer_depth = columns.TemplateColumn(
template_code=OUTER_UNIT,
verbose_name=_('Outer Depth')
)
weight = columns.TemplateColumn(
@@ -183,8 +191,9 @@ class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
fields = (
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role',
'rack_type', 'serial', 'asset_tag', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width',
'outer_depth', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'comments', 'device_count',
'get_utilization', 'get_power_utilization', 'description', 'contacts', 'tags', 'created', 'last_updated',
'outer_height', 'outer_depth', 'mounting_depth', 'airflow', 'weight', 'max_weight', 'comments',
'device_count', 'get_utilization', 'get_power_utilization', 'description', 'contacts',
'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'rack_type', 'u_height',

View File

@@ -109,6 +109,11 @@ LOCATION_BUTTONS = """
</a>
"""
OUTER_UNIT = """
{% load helpers %}
{% if value %}{{ value }} {{ record.outer_unit }}{% endif %}
"""
#
# Device component templatebuttons
#

View File

@@ -585,6 +585,7 @@ class RackTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
starting_unit=1,
desc_units=False,
outer_width=100,
outer_height=100,
outer_depth=100,
outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER,
mounting_depth=100,
@@ -603,6 +604,7 @@ class RackTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
starting_unit=2,
desc_units=False,
outer_width=200,
outer_height=200,
outer_depth=200,
outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER,
mounting_depth=200,
@@ -621,6 +623,7 @@ class RackTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
starting_unit=3,
desc_units=True,
outer_width=300,
outer_height=300,
outer_depth=300,
outer_unit=RackDimensionUnitChoices.UNIT_INCH,
mounting_depth=300,
@@ -681,6 +684,10 @@ class RackTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'outer_width': [100, 200]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_outer_height(self):
params = {'outer_height': [100, 200]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_outer_depth(self):
params = {'outer_depth': [100, 200]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@@ -764,6 +771,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
starting_unit=1,
desc_units=False,
outer_width=100,
outer_height=100,
outer_depth=100,
outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER,
mounting_depth=100,
@@ -782,6 +790,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
starting_unit=2,
desc_units=False,
outer_width=200,
outer_height=200,
outer_depth=200,
outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER,
mounting_depth=200,
@@ -831,6 +840,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
u_height=42,
desc_units=False,
outer_width=100,
outer_height=100,
outer_depth=100,
outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER,
weight=10,
@@ -854,6 +864,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
u_height=43,
desc_units=False,
outer_width=200,
outer_height=200,
outer_depth=200,
outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER,
weight=20,
@@ -877,6 +888,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
u_height=44,
desc_units=True,
outer_width=300,
outer_height=300,
outer_depth=300,
outer_unit=RackDimensionUnitChoices.UNIT_INCH,
weight=30,
@@ -957,6 +969,10 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'outer_width': [100, 200]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_outer_height(self):
params = {'outer_height': [100, 200]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_outer_depth(self):
params = {'outer_depth': [100, 200]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)