diff --git a/CHANGELOG.md b/CHANGELOG.md index 91a4059ba92..40be337fb51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ v2.5.0 (FUTURE) ## Enhancements +* [#450](https://github.com/digitalocean/netbox/issues/450) - Added `outer_width` and `outer_depth` fields to rack model * [#1444](https://github.com/digitalocean/netbox/issues/1444) - Added an `asset_tag` field for racks * [#2000](https://github.com/digitalocean/netbox/issues/2000) - Dropped support for Python 2 * [#2104](https://github.com/digitalocean/netbox/issues/2104) - Added a `status` field for racks diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index d1cb8bd39fc..e86a4549db0 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -121,14 +121,15 @@ class RackSerializer(TaggitSerializer, CustomFieldModelSerializer): role = NestedRackRoleSerializer(required=False, allow_null=True) type = ChoiceField(choices=RACK_TYPE_CHOICES, required=False, allow_null=True) width = ChoiceField(choices=RACK_WIDTH_CHOICES, required=False) + outer_unit = ChoiceField(choices=RACK_DIMENSION_UNIT_CHOICES, required=False) tags = TagListSerializerField(required=False) class Meta: model = Rack fields = [ 'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'status', 'role', 'serial', - 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'comments', 'tags', 'custom_fields', 'created', - 'last_updated', + 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', + 'comments', 'tags', 'custom_fields', 'created', 'last_updated', ] # Omit the UniqueTogetherValidator that would be automatically added to validate (group, facility_id). This # prevents facility_id from being interpreted as a required field. @@ -504,7 +505,7 @@ class CableSerializer(ValidatedModelSerializer): termination_a = serializers.SerializerMethodField(read_only=True) termination_b = serializers.SerializerMethodField(read_only=True) status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False) - length_unit = ChoiceField(choices=LENGTH_UNIT_CHOICES, required=False) + length_unit = ChoiceField(choices=CABLE_LENGTH_UNIT_CHOICES, required=False) class Meta: model = Cable diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 4e2050d44e1..16105587940 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -362,11 +362,16 @@ LENGTH_UNIT_METER = 'm' LENGTH_UNIT_CENTIMETER = 'cm' +LENGTH_UNIT_MILLIMETER = 'mm' LENGTH_UNIT_FOOT = 'ft' LENGTH_UNIT_INCH = 'in' -LENGTH_UNIT_CHOICES = ( +CABLE_LENGTH_UNIT_CHOICES = ( (LENGTH_UNIT_METER, 'Meters'), (LENGTH_UNIT_CENTIMETER, 'Centimeters'), (LENGTH_UNIT_FOOT, 'Feet'), (LENGTH_UNIT_INCH, 'Inches'), ) +RACK_DIMENSION_UNIT_CHOICES = ( + (LENGTH_UNIT_MILLIMETER, 'Millimeters'), + (LENGTH_UNIT_INCH, 'Inches'), +) diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index a6143065c1b..df2428e232c 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -201,7 +201,10 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet): class Meta: model = Rack - fields = ['name', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units'] + fields = [ + 'name', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', + 'outer_unit', + ] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 770f3ab8a6a..3fd6f85c55b 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -307,7 +307,7 @@ class Meta: model = Rack fields = [ 'site', 'group', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial', 'asset_tag', - 'type', 'width', 'u_height', 'desc_units', 'comments', 'tags', + 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments', 'tags', ] help_texts = { 'site': "The site at which the rack exists", @@ -368,6 +368,11 @@ class RackCSVForm(forms.ModelForm): ), help_text='Rail-to-rail width (in inches)' ) + outer_unit = CSVChoiceField( + choices=RACK_DIMENSION_UNIT_CHOICES, + required=False, + help_text='Unit for outer dimensions' + ) class Meta: model = Rack @@ -458,12 +463,26 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditFor widget=BulkEditNullBooleanSelect, label='Descending units' ) + outer_width = forms.IntegerField( + required=False, + min_value=1 + ) + outer_depth = forms.IntegerField( + required=False, + min_value=1 + ) + outer_unit = forms.ChoiceField( + choices=add_blank_choice(RACK_DIMENSION_UNIT_CHOICES), + required=False + ) comments = CommentField( widget=SmallTextarea ) class Meta: - nullable_fields = ['group', 'tenant', 'role', 'serial', 'asset_tag', 'comments'] + nullable_fields = [ + 'group', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'comments', + ] class RackFilterForm(BootstrapMixin, CustomFieldFilterForm): @@ -1864,7 +1883,7 @@ class CableCSVForm(forms.ModelForm): help_text='Cable type' ) length_unit = CSVChoiceField( - choices=LENGTH_UNIT_CHOICES, + choices=CABLE_LENGTH_UNIT_CHOICES, required=False, help_text='Length unit' ) @@ -1962,7 +1981,7 @@ class CableBulkEditForm(BootstrapMixin, BulkEditForm): required=False ) length_unit = forms.ChoiceField( - choices=add_blank_choice(LENGTH_UNIT_CHOICES), + choices=add_blank_choice(CABLE_LENGTH_UNIT_CHOICES), required=False, initial='' ) diff --git a/netbox/dcim/migrations/0068_rack_new_fields.py b/netbox/dcim/migrations/0068_rack_new_fields.py index b0a3e195814..e577b1a5217 100644 --- a/netbox/dcim/migrations/0068_rack_new_fields.py +++ b/netbox/dcim/migrations/0068_rack_new_fields.py @@ -20,4 +20,19 @@ class Migration(migrations.Migration): name='asset_tag', field=utilities.fields.NullableCharField(blank=True, max_length=50, null=True, unique=True), ), + migrations.AddField( + model_name='rack', + name='outer_depth', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='rack', + name='outer_unit', + field=models.CharField(blank=True, max_length=2), + ), + migrations.AddField( + model_name='rack', + name='outer_width', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index da8ee29bc6f..0bfd6abe9d8 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -510,6 +510,19 @@ class Rack(ChangeLoggedModel, CustomFieldModel): verbose_name='Descending units', help_text='Units are numbered top-to-bottom' ) + outer_width = models.PositiveSmallIntegerField( + blank=True, + null=True + ) + outer_depth = models.PositiveSmallIntegerField( + blank=True, + null=True + ) + outer_unit = models.CharField( + choices=RACK_DIMENSION_UNIT_CHOICES, + max_length=2, + blank=True + ) comments = models.TextField( blank=True ) @@ -527,7 +540,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel): csv_headers = [ 'site', 'group_name', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag', 'width', - 'u_height', 'desc_units', 'comments', + 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'comments', ] class Meta: @@ -545,6 +558,14 @@ def get_absolute_url(self): def clean(self): + # Validate outer dimensions and unit + if self.outer_width and not self.outer_unit: + raise ValidationError("Must specify a unit when setting an outer width") + if self.outer_depth and not self.outer_unit: + raise ValidationError("Must specify a unit when setting an outer depth") + if self.outer_unit and self.outer_width is None and self.outer_depth is None: + self.length_unit = '' + if self.pk: # Validate that Rack is tall enough to house the installed Devices top_device = Device.objects.filter(rack=self).exclude(position__isnull=True).order_by('-position').first() @@ -591,6 +612,9 @@ def to_csv(self): self.width, self.u_height, self.desc_units, + self.outer_width, + self.outer_depth, + self.outer_unit, self.comments, ) @@ -2410,7 +2434,7 @@ class Cable(ChangeLoggedModel): null=True ) length_unit = models.CharField( - choices=LENGTH_UNIT_CHOICES, + choices=CABLE_LENGTH_UNIT_CHOICES, max_length=2, blank=True ) diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 9427eb60057..2e17d04e7d9 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -172,6 +172,26 @@

{% block title %}Rack {{ rack }}{% endblock %}

Height {{ rack.u_height }}U ({% if rack.desc_units %}descending{% else %}ascending{% endif %}) + + Outer Width + + {% if rack.outer_width %} + {{ rack.outer_width }}{{ rack.outer_unit }} + {% else %} + + {% endif %} + + + + Outer Depth + + {% if rack.outer_depth %} + {{ rack.outer_depth }}{{ rack.outer_unit }} + {% else %} + + {% endif %} + + {% include 'inc/custom_fields_panel.html' with obj=rack %} diff --git a/netbox/templates/dcim/rack_edit.html b/netbox/templates/dcim/rack_edit.html index 798de03137d..cd1192c19cf 100644 --- a/netbox/templates/dcim/rack_edit.html +++ b/netbox/templates/dcim/rack_edit.html @@ -28,6 +28,18 @@ {% render_field form.type %} {% render_field form.width %} {% render_field form.u_height %} +
+ +
+ {{ form.outer_width }} +
+
+ {{ form.outer_depth }} +
+
+ {{ form.outer_unit }} +
+
{% render_field form.desc_units %}