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 "Rack" %}
+ + + + + +
{% trans "Description" %}{{ object.description|placeholder }}
+
+
+
{% trans "Dimensions" %}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{% 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 %}