Просмотр исходного кода

12175 rack with starting unit > 1 (#12778)

* 12175 add rack starting unit

* 12175 rack starting unit to svg

* verify devices can still fit if change rack starting_unit

* 12175 fix migration

* 12175 fix typo and test

* 12175 fix test

* 12175 fix max height calc display

* Misc cleanup & fixes

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
Arthur Hanson 2 лет назад
Родитель
Сommit
eff4a3741c

+ 2 - 0
netbox/dcim/constants.py

@@ -17,6 +17,8 @@ RACK_ELEVATION_BORDER_WIDTH = 2
 RACK_ELEVATION_DEFAULT_LEGEND_WIDTH = 30
 RACK_ELEVATION_DEFAULT_MARGIN_WIDTH = 15
 
+RACK_STARTING_UNIT_DEFAULT = 1
+
 
 #
 # RearPorts

+ 2 - 2
netbox/dcim/forms/model_forms.py

@@ -221,8 +221,8 @@ class RackForm(TenancyForm, NetBoxModelForm):
         model = Rack
         fields = [
             'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
-            'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
-            'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
+            'asset_tag', 'type', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth',
+            'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
         ]
 
 

+ 17 - 0
netbox/dcim/migrations/0174_rack_starting_unit.py

@@ -0,0 +1,17 @@
+# Generated by Django 4.1.9 on 2023-05-31 15:47
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('dcim', '0174_device_latitude_device_longitude'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='rack',
+            name='starting_unit',
+            field=models.PositiveSmallIntegerField(default=1),
+        ),
+    ]

+ 22 - 13
netbox/dcim/models/racks.py

@@ -129,6 +129,11 @@ class Rack(PrimaryModel, WeightMixin):
         validators=[MinValueValidator(1), MaxValueValidator(RACK_U_HEIGHT_MAX)],
         help_text=_('Height in rack units')
     )
+    starting_unit = models.PositiveSmallIntegerField(
+        default=RACK_STARTING_UNIT_DEFAULT,
+        verbose_name='Starting unit',
+        help_text=_('Starting unit for rack')
+    )
     desc_units = models.BooleanField(
         default=False,
         verbose_name='Descending units',
@@ -228,20 +233,24 @@ class Rack(PrimaryModel, WeightMixin):
             raise ValidationError("Must specify a unit when setting a maximum weight")
 
         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()
-            if top_device:
-                min_height = top_device.position + top_device.device_type.u_height - 1
+            mounted_devices = Device.objects.filter(rack=self).exclude(position__isnull=True).order_by('position')
+
+            # Validate that Rack is tall enough to house the highest mounted Device
+            if top_device := mounted_devices.last():
+                min_height = top_device.position + top_device.device_type.u_height - self.starting_unit
                 if self.u_height < min_height:
                     raise ValidationError({
-                        'u_height': "Rack must be at least {}U tall to house currently installed devices.".format(
-                            min_height
-                        )
+                        'u_height': f"Rack must be at least {min_height}U tall to house currently installed devices."
                     })
+
+            # Validate that the Rack's starting unit is less than or equal to the position of the lowest mounted Device
+            if last_device := mounted_devices.first():
+                if self.starting_unit > last_device.position:
+                    raise ValidationError({
+                        'starting_unit': f"Rack unit numbering must begin at {last_device.position} or less to house "
+                                         f"currently installed devices."
+                    })
+
             # Validate that Rack was assigned a Location of its same site, if applicable
             if self.location:
                 if self.location.site != self.site:
@@ -269,8 +278,8 @@ class Rack(PrimaryModel, WeightMixin):
         Return a list of unit numbers, top to bottom.
         """
         if self.desc_units:
-            return drange(decimal.Decimal(1.0), self.u_height + 1, 0.5)
-        return drange(self.u_height + decimal.Decimal(0.5), 0.5, -0.5)
+            return drange(decimal.Decimal(self.starting_unit), self.u_height + self.starting_unit, 0.5)
+        return drange(self.u_height + decimal.Decimal(0.5) + self.starting_unit - 1, 0.5 + self.starting_unit - 1, -0.5)
 
     def get_status_color(self):
         return RackStatusChoices.colors.get(self.status)

+ 4 - 2
netbox/dcim/svg/racks.py

@@ -150,9 +150,9 @@ class RackElevationSVG:
         x = self.legend_width + RACK_ELEVATION_BORDER_WIDTH
         y = RACK_ELEVATION_BORDER_WIDTH
         if self.rack.desc_units:
-            y += int((position - 1) * self.unit_height)
+            y += int((position - self.rack.starting_unit) * self.unit_height)
         else:
-            y += int((self.rack.u_height - position + 1) * self.unit_height) - int(height * self.unit_height)
+            y += int((self.rack.u_height - position + self.rack.starting_unit) * self.unit_height) - int(height * self.unit_height)
 
         return x, y
 
@@ -237,6 +237,7 @@ class RackElevationSVG:
             start_y = ru * self.unit_height + RACK_ELEVATION_BORDER_WIDTH
             position_coordinates = (self.legend_width / 2, start_y + self.unit_height / 2 + RACK_ELEVATION_BORDER_WIDTH)
             unit = ru + 1 if self.rack.desc_units else self.rack.u_height - ru
+            unit = unit + self.rack.starting_unit - 1
             self.drawing.add(
                 Text(str(unit), position_coordinates, class_='unit')
             )
@@ -278,6 +279,7 @@ class RackElevationSVG:
 
         for ru in range(0, self.rack.u_height):
             unit = ru + 1 if self.rack.desc_units else self.rack.u_height - ru
+            unit = unit + self.rack.starting_unit - 1
             y_offset = RACK_ELEVATION_BORDER_WIDTH + ru * self.unit_height
             text_coords = (
                 x_offset + self.unit_width / 2,

+ 1 - 0
netbox/dcim/tests/test_views.py

@@ -392,6 +392,7 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             'outer_width': 500,
             'outer_depth': 500,
             'outer_unit': RackDimensionUnitChoices.UNIT_MILLIMETER,
+            'starting_unit': 1,
             'weight': 100,
             'max_weight': 2000,
             'weight_unit': WeightUnitChoices.UNIT_POUND,

+ 6 - 0
netbox/templates/dcim/rack.html

@@ -101,6 +101,12 @@
                         <th scope="row">Height</th>
                         <td>{{ object.u_height }}U ({% if object.desc_units %}descending{% else %}ascending{% endif %})</td>
                     </tr>
+                    <tr>
+                        <th scope="row">Starting Unit</th>
+                        <td>
+                          {{ object.starting_unit }}
+                        </td>
+                    </tr>
                     <tr>
                         <th scope="row">Outer Width</th>
                         <td>

+ 1 - 0
netbox/templates/dcim/rack_edit.html

@@ -71,6 +71,7 @@
         </div>
         {% render_field form.mounting_depth %}
         {% render_field form.desc_units %}
+        {% render_field form.starting_unit %}
     </div>
 
     {% if form.custom_fields %}