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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 128 additions and 49 deletions

View File

@ -40,7 +40,7 @@ The number of the numerically lowest unit in the rack. This value defaults to on
### Outer Dimensions
The external width and depth of the rack can be tracked to aid in floorplan calculations. These measurements must be designated in either millimeters or inches.
The external width, height and depth of the rack can be tracked to aid in floorplan calculations. These measurements must be designated in either millimeters or inches.
### Mounting Depth

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)

View File

@ -24,6 +24,16 @@
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Outer Height" %}</th>
<td>
{% if object.outer_height %}
{{ object.outer_height }} {{ object.get_outer_unit_display }}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Outer Depth" %}</th>
<td>