diff --git a/netbox/dcim/api/serializers_/racks.py b/netbox/dcim/api/serializers_/racks.py
index 64856d5cd..f48aabf07 100644
--- a/netbox/dcim/api/serializers_/racks.py
+++ b/netbox/dcim/api/serializers_/racks.py
@@ -3,7 +3,7 @@ from rest_framework import serializers
from dcim.choices import *
from dcim.constants import *
-from dcim.models import Rack, RackReservation, RackRole
+from dcim.models import Rack, RackReservation, RackRole, RackType
from netbox.api.fields import ChoiceField, RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer
from netbox.config import ConfigItem
@@ -41,7 +41,7 @@ class RackTypeSerializer(NetBoxModelSerializer):
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
class Meta:
- model = Rack
+ model = RackType
fields = [
'id', 'url', 'display_url', 'display', 'name',
'type', 'width', 'u_height', 'starting_unit', 'weight', 'max_weight',
diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py
index 612b6e858..783d7ccab 100644
--- a/netbox/dcim/filtersets.py
+++ b/netbox/dcim/filtersets.py
@@ -297,19 +297,9 @@ class RackTypeFilterSet(NetBoxModelFilterSet):
width = django_filters.MultipleChoiceFilter(
choices=RackWidthChoices
)
- role_id = django_filters.ModelMultipleChoiceFilter(
- queryset=RackRole.objects.all(),
- label=_('Role (ID)'),
- )
- role = django_filters.ModelMultipleChoiceFilter(
- field_name='role__slug',
- queryset=RackRole.objects.all(),
- to_field_name='slug',
- label=_('Role (slug)'),
- )
class Meta:
- model = Rack
+ model = RackType
fields = (
'id', 'name', 'u_height', 'starting_unit', 'desc_units', 'outer_width',
'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description',
diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py
index d25f4d5cf..7e95b9ce7 100644
--- a/netbox/dcim/forms/filtersets.py
+++ b/netbox/dcim/forms/filtersets.py
@@ -241,11 +241,10 @@ class RackRoleFilterForm(NetBoxModelFilterSetForm):
class RackTypeFilterForm(NetBoxModelFilterSetForm):
- model = Rack
+ model = RackType
fieldsets = (
FieldSet('q', 'filter_id', 'tag'),
- FieldSet('role_id', name=_('Function')),
- FieldSet('type', 'width', 'serial', name=_('Hardware')),
+ FieldSet('type', 'width', name=_('Hardware')),
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
)
selector_fields = ('filter_id', 'q',)
@@ -259,16 +258,6 @@ class RackTypeFilterForm(NetBoxModelFilterSetForm):
choices=RackWidthChoices,
required=False
)
- role_id = DynamicModelMultipleChoiceField(
- queryset=RackRole.objects.all(),
- required=False,
- null_option='None',
- label=_('Role')
- )
- serial = forms.CharField(
- label=_('Serial'),
- required=False
- )
tag = TagFilterField(model)
weight = forms.DecimalField(
label=_('Weight'),
diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py
index 69ec058f7..b4292dbc1 100644
--- a/netbox/dcim/models/racks.py
+++ b/netbox/dcim/models/racks.py
@@ -120,7 +120,7 @@ class RackType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
)
clone_fields = (
- 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
+ 'type', 'width', 'u_height', 'desc_units', 'outer_width',
'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
)
prerequisite_models = (
@@ -172,21 +172,6 @@ class RackType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
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)
- @cached_property
- def total_weight(self):
- total_weight = sum(
- device.device_type._abs_weight
- for device in self.devices.exclude(device_type___abs_weight__isnull=True).prefetch_related('device_type')
- )
- total_weight += sum(
- module.module_type._abs_weight
- for module in Module.objects.filter(device__rack=self)
- .exclude(module_type___abs_weight__isnull=True)
- .prefetch_related('module_type')
- )
- if self._abs_weight:
- total_weight += self._abs_weight
- return round(total_weight / 1000, 2)
#
# Racks
diff --git a/netbox/dcim/search.py b/netbox/dcim/search.py
index b349bcac0..34a9e73ed 100644
--- a/netbox/dcim/search.py
+++ b/netbox/dcim/search.py
@@ -242,6 +242,19 @@ class PowerPortIndex(SearchIndex):
display_attrs = ('device', 'label', 'type', 'description')
+@register_search
+class RackTypeIndex(SearchIndex):
+ model = models.RackType
+ fields = (
+ ('name', 100),
+ ('description', 500),
+ ('comments', 5000),
+ )
+ display_attrs = (
+ 'site', 'description',
+ )
+
+
@register_search
class RackIndex(SearchIndex):
model = models.Rack
diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py
index 52b850b24..61c0f4039 100644
--- a/netbox/dcim/tests/test_api.py
+++ b/netbox/dcim/tests/test_api.py
@@ -274,6 +274,36 @@ class RackRoleTest(APIViewTestCases.APIViewTestCase):
RackRole.objects.bulk_create(rack_roles)
+class RackTypeTest(APIViewTestCases.APIViewTestCase):
+ model = RackType
+ brief_fields = ['description', 'display', 'id', 'name', 'url']
+ bulk_update_data = {
+ 'description': 'new description',
+ }
+
+ @classmethod
+ def setUpTestData(cls):
+
+ racks = (
+ RackType(name='Rack 1'),
+ RackType(name='Rack 2'),
+ RackType(name='Rack 3'),
+ )
+ RackType.objects.bulk_create(racks)
+
+ cls.create_data = [
+ {
+ 'name': 'Test Rack 4',
+ },
+ {
+ 'name': 'Test Rack 5',
+ },
+ {
+ 'name': 'Test Rack 6',
+ },
+ ]
+
+
class RackTest(APIViewTestCases.APIViewTestCase):
model = Rack
brief_fields = ['description', 'device_count', 'display', 'id', 'name', 'url']
diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py
index df0dc7c7e..8bcad5552 100644
--- a/netbox/dcim/tests/test_filtersets.py
+++ b/netbox/dcim/tests/test_filtersets.py
@@ -468,6 +468,121 @@ class RackRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+class RackTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
+ queryset = RackType.objects.all()
+ filterset = RackTypeFilterSet
+
+ @classmethod
+ def setUpTestData(cls):
+
+ racks = (
+ RackType(
+ name='Rack 1',
+ type=RackTypeChoices.TYPE_2POST,
+ width=RackWidthChoices.WIDTH_19IN,
+ u_height=42,
+ desc_units=False,
+ outer_width=100,
+ outer_depth=100,
+ outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER,
+ weight=10,
+ max_weight=1000,
+ weight_unit=WeightUnitChoices.UNIT_POUND,
+ description='foobar1'
+ ),
+ RackType(
+ name='Rack 2',
+ type=RackTypeChoices.TYPE_4POST,
+ width=RackWidthChoices.WIDTH_21IN,
+ u_height=43,
+ desc_units=False,
+ outer_width=200,
+ outer_depth=200,
+ outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER,
+ weight=20,
+ max_weight=2000,
+ weight_unit=WeightUnitChoices.UNIT_POUND,
+ description='foobar2'
+ ),
+ RackType(
+ name='Rack 3',
+ type=RackTypeChoices.TYPE_CABINET,
+ width=RackWidthChoices.WIDTH_23IN,
+ u_height=44,
+ desc_units=True,
+ outer_width=300,
+ outer_depth=300,
+ outer_unit=RackDimensionUnitChoices.UNIT_INCH,
+ weight=30,
+ max_weight=3000,
+ weight_unit=WeightUnitChoices.UNIT_KILOGRAM,
+ description='foobar3'
+ ),
+ )
+ RackType.objects.bulk_create(racks)
+
+ def test_q(self):
+ params = {'q': 'foobar1'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+
+ def test_name(self):
+ params = {'name': ['Rack 1', 'Rack 2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_description(self):
+ params = {'description': ['foobar1', 'foobar2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_type(self):
+ params = {'type': [RackTypeChoices.TYPE_2POST, RackTypeChoices.TYPE_4POST]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_width(self):
+ params = {'width': [RackWidthChoices.WIDTH_19IN, RackWidthChoices.WIDTH_21IN]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_u_height(self):
+ params = {'u_height': [42, 43]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_starting_unit(self):
+ params = {'starting_unit': [1]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+ params = {'starting_unit': [2]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0)
+
+ def test_desc_units(self):
+ params = {'desc_units': 'true'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
+ params = {'desc_units': 'false'}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_outer_width(self):
+ params = {'outer_width': [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)
+
+ def test_outer_unit(self):
+ self.assertEqual(Rack.objects.filter(outer_unit__isnull=False).count(), 3)
+ params = {'outer_unit': RackDimensionUnitChoices.UNIT_MILLIMETER}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_weight(self):
+ params = {'weight': [10, 20]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_max_weight(self):
+ params = {'max_weight': [1000, 2000]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_weight_unit(self):
+ params = {'weight_unit': WeightUnitChoices.UNIT_POUND}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+
class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Rack.objects.all()
filterset = RackFilterSet
diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py
index 9056a66c0..bdcb0ad4a 100644
--- a/netbox/dcim/tests/test_models.py
+++ b/netbox/dcim/tests/test_models.py
@@ -74,6 +74,17 @@ class LocationTestCase(TestCase):
self.assertEqual(PowerPanel.objects.get(pk=powerpanel1.pk).site, site_b)
+class RackTypeTestCase(TestCase):
+
+ @classmethod
+ def setUpTestData(cls):
+
+ RackType.objects.create(
+ name='Rack 1',
+ u_height=42
+ )
+
+
class RackTestCase(TestCase):
@classmethod
diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py
index ec85fc1d5..7b1f8a261 100644
--- a/netbox/dcim/tests/test_views.py
+++ b/netbox/dcim/tests/test_views.py
@@ -336,6 +336,67 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
}
+class RackTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
+ model = RackType
+
+ @classmethod
+ def setUpTestData(cls):
+
+ racks = (
+ Rack(name='Rack 1'),
+ Rack(name='Rack 2'),
+ Rack(name='Rack 3'),
+ )
+ RackType.objects.bulk_create(racks)
+
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
+ cls.form_data = {
+ 'name': 'Rack X',
+ 'type': RackTypeChoices.TYPE_CABINET,
+ 'width': RackWidthChoices.WIDTH_19IN,
+ 'u_height': 48,
+ 'desc_units': False,
+ 'outer_width': 500,
+ 'outer_depth': 500,
+ 'outer_unit': RackDimensionUnitChoices.UNIT_MILLIMETER,
+ 'starting_unit': 1,
+ 'weight': 100,
+ 'max_weight': 2000,
+ 'weight_unit': WeightUnitChoices.UNIT_POUND,
+ 'comments': 'Some comments',
+ 'tags': [t.pk for t in tags],
+ }
+
+ cls.csv_data = (
+ "name,width,u_height,weight,max_weight,weight_unit",
+ ",Rack 4,19,42,100,2000,kg",
+ "Rack 5,19,42,100,2000,kg",
+ "Rack 6,19,42,100,2000,kg",
+ )
+
+ cls.csv_update_data = (
+ "id,name",
+ f"{racks[0].pk},Rack 7",
+ f"{racks[1].pk},Rack 8",
+ f"{racks[2].pk},Rack 9",
+ )
+
+ cls.bulk_edit_data = {
+ 'type': RackTypeChoices.TYPE_4POST,
+ 'width': RackWidthChoices.WIDTH_23IN,
+ 'u_height': 49,
+ 'desc_units': True,
+ 'outer_width': 30,
+ 'outer_depth': 30,
+ 'outer_unit': RackDimensionUnitChoices.UNIT_INCH,
+ 'weight': 200,
+ 'max_weight': 4000,
+ 'weight_unit': WeightUnitChoices.UNIT_POUND,
+ 'comments': 'New comments',
+ }
+
+
class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = Rack
diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py
index 695ed3054..42b1ba098 100644
--- a/netbox/dcim/views.py
+++ b/netbox/dcim/views.py
@@ -592,7 +592,7 @@ class RackTypeListView(generic.ObjectListView):
@register_model_view(RackType)
class RackTypeView(GetRelatedModelsMixin, generic.ObjectView):
- queryset = Rack.objects.prefetch_related('role')
+ queryset = RackType.objects.all()
@register_model_view(RackType, 'edit')
diff --git a/netbox/templates/dcim/racktype.html b/netbox/templates/dcim/racktype.html
new file mode 100644
index 000000000..141aa4701
--- /dev/null
+++ b/netbox/templates/dcim/racktype.html
@@ -0,0 +1,123 @@
+{% extends 'dcim/rack/base.html' %}
+{% load buttons %}
+{% load helpers %}
+{% load static %}
+{% load plugins %}
+{% load i18n %}
+{% load mptt %}
+
+{% block content %}
+
+
+
+
+
+
+ {% trans "Description" %} |
+ {{ object.description|placeholder }} |
+
+
+
+
+
+
+
+ {% trans "Type" %} |
+
+ {% if object.type %}
+ {{ object.get_type_display }}
+ {% else %}
+ {{ ''|placeholder }}
+ {% endif %}
+ |
+
+
+ {% trans "Width" %} |
+ {{ object.get_width_display }} |
+
+
+ {% trans "Height" %} |
+ {{ object.u_height }}U ({% if object.desc_units %}{% trans "descending" %}{% else %}{% trans "ascending" %}{% endif %}) |
+
+
+ {% trans "Starting Unit" %} |
+
+ {{ object.starting_unit }}
+ |
+
+
+ {% trans "Outer Width" %} |
+
+ {% if object.outer_width %}
+ {{ object.outer_width }} {{ object.get_outer_unit_display }}
+ {% else %}
+ {{ ''|placeholder }}
+ {% endif %}
+ |
+
+
+ {% trans "Outer Depth" %} |
+
+ {% if object.outer_depth %}
+ {{ object.outer_depth }} {{ object.get_outer_unit_display }}
+ {% else %}
+ {{ ''|placeholder }}
+ {% endif %}
+ |
+
+
+ {% trans "Mounting Depth" %} |
+
+ {% if object.mounting_depth %}
+ {{ object.mounting_depth }} {% trans "Millimeters" %}
+ {% else %}
+ {{ ''|placeholder }}
+ {% endif %}
+ |
+
+
+ {% trans "Rack Weight" %} |
+
+ {% if object.weight %}
+ {{ object.weight|floatformat }} {{ object.get_weight_unit_display }}
+ {% else %}
+ {{ ''|placeholder }}
+ {% endif %}
+ |
+
+
+ {% trans "Maximum Weight" %} |
+
+ {% if object.max_weight %}
+ {{ object.max_weight }} {{ object.get_weight_unit_display }}
+ {% else %}
+ {{ ''|placeholder }}
+ {% endif %}
+ |
+
+
+
+ {% include 'inc/panels/custom_fields.html' %}
+ {% include 'inc/panels/tags.html' %}
+ {% include 'inc/panels/comments.html' %}
+ {% include 'inc/panels/image_attachments.html' %}
+ {% plugin_left_page object %}
+
+
+
+
+
+ {% include 'inc/panels/related_objects.html' %}
+ {% plugin_right_page object %}
+
+
+
+
+ {% plugin_full_width_page object %}
+
+
+{% endblock %}