diff --git a/docs/models/dcim/racktype.md b/docs/models/dcim/racktype.md index ccbf9ffc4..4944e7021 100644 --- a/docs/models/dcim/racktype.md +++ b/docs/models/dcim/racktype.md @@ -4,6 +4,10 @@ A rack type defines the physical characteristics of a particular model of [rack] ## Fields +### Manufacturer + +The [manufacturer](./manufacturer.md) which produces this type of rack. + ### Name The unique name of the rack type. diff --git a/netbox/dcim/api/serializers_/racks.py b/netbox/dcim/api/serializers_/racks.py index c66e8253d..c844be999 100644 --- a/netbox/dcim/api/serializers_/racks.py +++ b/netbox/dcim/api/serializers_/racks.py @@ -9,6 +9,7 @@ from netbox.api.serializers import NetBoxModelSerializer from netbox.config import ConfigItem from tenancy.api.serializers_.tenants import TenantSerializer from users.api.serializers_.users import UserSerializer +from .manufacturers import ManufacturerSerializer from .sites import LocationSerializer, SiteSerializer __all__ = ( @@ -35,6 +36,9 @@ class RackRoleSerializer(NetBoxModelSerializer): class RackTypeSerializer(NetBoxModelSerializer): + manufacturer = ManufacturerSerializer( + nested=True + ) type = ChoiceField( choices=RackTypeChoices, allow_blank=True, @@ -61,12 +65,12 @@ class RackTypeSerializer(NetBoxModelSerializer): class Meta: model = RackType fields = [ - 'id', 'url', 'display_url', 'display', 'name', 'slug', 'description', 'type', 'width', 'u_height', - 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'weight', 'max_weight', - 'weight_unit', 'mounting_depth', 'description', 'comments', 'tags', 'custom_fields', 'created', - 'last_updated', + 'id', 'url', 'display_url', 'display', 'manufacturer', 'name', 'slug', 'description', 'type', 'width', + 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'weight', + 'max_weight', 'weight_unit', 'mounting_depth', 'description', 'comments', 'tags', 'custom_fields', + 'created', 'last_updated', ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description') + brief_fields = ('id', 'url', 'display', 'manufacturer', 'name', 'slug', 'description') class RackSerializer(NetBoxModelSerializer): diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 0558922a5..7452ca69d 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -291,6 +291,16 @@ class RackRoleFilterSet(OrganizationalModelFilterSet): class RackTypeFilterSet(NetBoxModelFilterSet): + manufacturer_id = django_filters.ModelMultipleChoiceFilter( + queryset=Manufacturer.objects.all(), + label=_('Manufacturer (ID)'), + ) + manufacturer = django_filters.ModelMultipleChoiceFilter( + field_name='manufacturer__slug', + queryset=Manufacturer.objects.all(), + to_field_name='slug', + label=_('Manufacturer (slug)'), + ) type = django_filters.MultipleChoiceFilter( choices=RackTypeChoices ) @@ -301,8 +311,8 @@ class RackTypeFilterSet(NetBoxModelFilterSet): class Meta: model = RackType fields = ( - 'id', 'name', 'slug', 'u_height', 'starting_unit', 'desc_units', 'outer_width', - 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description', + 'id', 'name', 'slug', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', + 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description', ) def search(self, queryset, name, value): diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 787d0bfa9..788f9c0e4 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -220,6 +220,11 @@ class RackRoleBulkEditForm(NetBoxModelBulkEditForm): class RackTypeBulkEditForm(NetBoxModelBulkEditForm): + manufacturer = DynamicModelChoiceField( + label=_('Manufacturer'), + queryset=Manufacturer.objects.all(), + required=False + ) type = forms.ChoiceField( label=_('Type'), choices=add_blank_choice(RackTypeChoices), @@ -288,12 +293,14 @@ class RackTypeBulkEditForm(NetBoxModelBulkEditForm): model = RackType fieldsets = ( - FieldSet('description', 'type', name=_('Rack Type')), + FieldSet('manufacturer', 'description', 'type', name=_('Rack Type')), FieldSet( - 'width', 'u_height', + 'width', + 'u_height', InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')), InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')), - 'mounting_depth', name=_('Dimensions') + 'mounting_depth', + name=_('Dimensions') ), FieldSet('starting_unit', 'desc_units', name=_('Numbering')), ) diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 71f0e2fde..9b5a6e57b 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -178,6 +178,12 @@ class RackRoleImportForm(NetBoxModelImportForm): class RackTypeImportForm(NetBoxModelImportForm): + manufacturer = forms.ModelChoiceField( + label=_('Manufacturer'), + queryset=Manufacturer.objects.all(), + to_field_name='name', + help_text=_('The manufacturer of this rack type') + ) type = CSVChoiceField( label=_('Type'), choices=RackTypeChoices, @@ -210,9 +216,9 @@ class RackTypeImportForm(NetBoxModelImportForm): class Meta: model = RackType fields = ( - 'name', 'slug', 'type', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', - 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', - 'max_weight', 'weight_unit', 'description', 'comments', 'tags', + 'manufacturer', 'name', 'slug', 'type', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', + 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description', + 'comments', 'tags', ) def __init__(self, data=None, *args, **kwargs): diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 9ea73e518..7a6989537 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -248,7 +248,12 @@ class RackTypeFilterForm(NetBoxModelFilterSetForm): FieldSet('type', 'width', 'u_height', 'starting_unit', name=_('Rack Type')), FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')), ) - selector_fields = ('filter_id', 'q',) + selector_fields = ('filter_id', 'q', 'manufacturer_id') + manufacturer_id = DynamicModelMultipleChoiceField( + queryset=Manufacturer.objects.all(), + required=False, + label=_('Manufacturer') + ) type = forms.MultipleChoiceField( label=_('Type'), choices=RackTypeChoices, diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index f880c54bf..ec2b3053f 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -203,11 +203,15 @@ class RackRoleForm(NetBoxModelForm): class RackTypeForm(NetBoxModelForm): + manufacturer = DynamicModelChoiceField( + label=_('Manufacturer'), + queryset=Manufacturer.objects.all() + ) comments = CommentField() slug = SlugField() fieldsets = ( - FieldSet('name', 'slug', 'description', 'type', 'tags', name=_('Rack')), + FieldSet('manufacturer', 'name', 'slug', 'description', 'type', 'tags', name=_('Rack Type')), FieldSet( 'width', 'u_height', InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')), @@ -220,9 +224,9 @@ class RackTypeForm(NetBoxModelForm): class Meta: model = RackType fields = [ - 'name', 'slug', 'type', 'width', 'u_height', 'starting_unit', 'desc_units', - 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', - 'weight_unit', 'description', 'comments', 'tags', + 'manufacturer', 'name', 'slug', 'type', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', + 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description', + 'comments', 'tags', ] diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 646b60a49..d2bf4b416 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -614,6 +614,7 @@ class PowerPortTemplateType(ModularComponentTemplateType): ) class RackTypeType(NetBoxObjectType): _name: str + manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')] @strawberry_django.type( diff --git a/netbox/dcim/migrations/0188_racktype.py b/netbox/dcim/migrations/0188_racktype.py index d5a477e17..c6ee36717 100644 --- a/netbox/dcim/migrations/0188_racktype.py +++ b/netbox/dcim/migrations/0188_racktype.py @@ -1,9 +1,8 @@ -# Generated by Django 4.2.11 on 2024-06-25 16:43 - import django.core.validators -from django.db import migrations, models import django.db.models.deletion import taggit.managers +from django.db import migrations, models + import utilities.fields import utilities.json import utilities.ordering @@ -23,41 +22,43 @@ class Migration(migrations.Migration): ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), ('created', models.DateTimeField(auto_now_add=True, null=True)), ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ( - 'custom_field_data', - models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder), - ), + ('custom_field_data', models.JSONField( + blank=True, + default=dict, + encoder=utilities.json.CustomFieldJSONEncoder + )), ('description', models.CharField(blank=True, max_length=200)), ('comments', models.TextField(blank=True)), ('weight', models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True)), ('weight_unit', models.CharField(blank=True, max_length=50)), ('_abs_weight', models.PositiveBigIntegerField(blank=True, null=True)), + ('manufacturer', models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name='rack_types', + to='dcim.manufacturer' + )), ('name', models.CharField(max_length=100)), - ( - '_name', - utilities.fields.NaturalOrderingField( - 'name', blank=True, max_length=100, naturalize_function=utilities.ordering.naturalize - ), + ('_name', utilities.fields.NaturalOrderingField( + 'name', + blank=True, + max_length=100, + naturalize_function=utilities.ordering.naturalize + ), ), ('slug', models.SlugField(max_length=100, unique=True)), ('type', models.CharField(blank=True, max_length=50)), ('width', models.PositiveSmallIntegerField(default=19)), - ( - 'u_height', - models.PositiveSmallIntegerField( - default=42, - validators=[ - django.core.validators.MinValueValidator(1), - django.core.validators.MaxValueValidator(100), - ], - ), - ), - ( - 'starting_unit', - models.PositiveSmallIntegerField( - default=1, validators=[django.core.validators.MinValueValidator(1)] - ), - ), + ('u_height', models.PositiveSmallIntegerField( + default=42, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(100), + ] + )), + ('starting_unit', models.PositiveSmallIntegerField( + default=1, + validators=[django.core.validators.MinValueValidator(1)] + )), ('desc_units', models.BooleanField(default=False)), ('outer_width', models.PositiveSmallIntegerField(blank=True, null=True)), ('outer_depth', models.PositiveSmallIntegerField(blank=True, null=True)), diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index b3cbee8b5..7a981971a 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -48,6 +48,11 @@ class RackType(WeightMixin, PrimaryModel): 'mounting_depth' ] + manufacturer = models.ForeignKey( + to='dcim.Manufacturer', + on_delete=models.PROTECT, + related_name='rack_types' + ) name = models.CharField( verbose_name=_('name'), max_length=100 @@ -83,7 +88,7 @@ class RackType(WeightMixin, PrimaryModel): starting_unit = models.PositiveSmallIntegerField( default=RACK_STARTING_UNIT_DEFAULT, verbose_name=_('starting unit'), - validators=[MinValueValidator(1),], + validators=[MinValueValidator(1)], help_text=_('Starting unit for rack') ) desc_units = models.BooleanField( @@ -107,7 +112,7 @@ class RackType(WeightMixin, PrimaryModel): verbose_name=_('outer unit'), max_length=50, choices=RackDimensionUnitChoices, - blank=True, + blank=True ) max_weight = models.PositiveIntegerField( verbose_name=_('max weight'), @@ -131,11 +136,11 @@ class RackType(WeightMixin, PrimaryModel): ) clone_fields = ( - 'type', 'width', 'u_height', 'desc_units', 'outer_width', - 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', + 'manufacturer', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', + 'mounting_depth', 'weight', 'max_weight', 'weight_unit', ) prerequisite_models = ( - 'dcim.Site', + 'dcim.Manufacturer', ) class Meta: @@ -205,7 +210,6 @@ class RackType(WeightMixin, PrimaryModel): # Racks # - class RackRole(OrganizationalModel): """ Racks can be organized by functional role, similar to Devices. diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py index 5d00636ed..858364aad 100644 --- a/netbox/dcim/tables/racks.py +++ b/netbox/dcim/tables/racks.py @@ -55,6 +55,10 @@ class RackTypeTable(NetBoxTable): order_by=('_name',), linkify=True ) + manufacturer = tables.Column( + verbose_name=_('Manufacturer'), + linkify=True + ) u_height = tables.TemplateColumn( template_code="{{ value }}U", verbose_name=_('Height') @@ -87,11 +91,12 @@ class RackTypeTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = RackType fields = ( - 'pk', 'id', 'name', 'type', 'u_height', 'starting_unit', 'width', 'outer_width', 'outer_depth', - 'mounting_depth', 'weight', 'max_weight', 'description', 'comments', 'tags', 'created', 'last_updated', + 'pk', 'id', 'name', 'manufacturer', 'type', 'u_height', 'starting_unit', 'width', 'outer_width', + 'outer_depth', 'mounting_depth', 'weight', 'max_weight', 'description', 'comments', 'tags', 'created', + 'last_updated', ) default_columns = ( - 'pk', 'name', 'type', 'u_height', 'description', + 'pk', 'name', 'manufacturer', 'type', 'u_height', 'description', ) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 68250dd83..8d14c077f 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -276,33 +276,41 @@ class RackRoleTest(APIViewTestCases.APIViewTestCase): class RackTypeTest(APIViewTestCases.APIViewTestCase): model = RackType - brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url'] + brief_fields = ['description', 'display', 'id', 'manufacturer', 'name', 'slug', 'url'] bulk_update_data = { 'description': 'new description', } @classmethod def setUpTestData(cls): - - racks = ( - RackType(name='RackType 1', slug='rack-type-1'), - RackType(name='RackType 2', slug='rack-type-2'), - RackType(name='RackType 3', slug='rack-type-3'), + manufacturers = ( + Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), + Manufacturer(name='Manufacturer 2', slug='manufacturer-2'), ) - RackType.objects.bulk_create(racks) + Manufacturer.objects.bulk_create(manufacturers) + + rack_types = ( + RackType(manufacturer=manufacturers[0], name='Rack Type 1', slug='rack-type-1'), + RackType(manufacturer=manufacturers[0], name='Rack Type 2', slug='rack-type-2'), + RackType(manufacturer=manufacturers[0], name='Rack Type 3', slug='rack-type-3'), + ) + RackType.objects.bulk_create(rack_types) cls.create_data = [ { - 'name': 'Test RackType 4', - 'slug': 'test-rack-type-4', + 'manufacturer': manufacturers[1].pk, + 'name': 'Rack Type 4', + 'slug': 'rack-type-4', }, { - 'name': 'Test RackType 5', - 'slug': 'test-rack-type-5', + 'manufacturer': manufacturers[1].pk, + 'name': 'Rack Type 5', + 'slug': 'rack-type-5', }, { - 'name': 'Test RackType 6', - 'slug': 'test-rack-type-6', + 'manufacturer': manufacturers[1].pk, + 'name': 'Rack Type 6', + 'slug': 'rack-type-6', }, ] diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index ea10d7086..3e427f3ea 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -474,9 +474,16 @@ class RackTypeTestCase(TestCase, ChangeLoggedFilterSetTests): @classmethod def setUpTestData(cls): + manufacturers = ( + Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), + Manufacturer(name='Manufacturer 2', slug='manufacturer-2'), + Manufacturer(name='Manufacturer 3', slug='manufacturer-3'), + ) + Manufacturer.objects.bulk_create(manufacturers) racks = ( RackType( + manufacturer=manufacturers[0], name='RackType 1', slug='rack-type-1', type=RackTypeChoices.TYPE_2POST, @@ -492,6 +499,7 @@ class RackTypeTestCase(TestCase, ChangeLoggedFilterSetTests): description='foobar1' ), RackType( + manufacturer=manufacturers[1], name='RackType 2', slug='rack-type-2', type=RackTypeChoices.TYPE_4POST, @@ -507,6 +515,7 @@ class RackTypeTestCase(TestCase, ChangeLoggedFilterSetTests): description='foobar2' ), RackType( + manufacturer=manufacturers[2], name='RackType 3', slug='rack-type-3', type=RackTypeChoices.TYPE_CABINET, @@ -528,6 +537,13 @@ class RackTypeTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'q': 'foobar1'} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + def test_manufacturer(self): + manufacturers = Manufacturer.objects.all()[:2] + params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'manufacturer': [manufacturers[0].slug, manufacturers[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_name(self): params = {'name': ['RackType 1', 'RackType 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 475cef2af..229edc0de 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -78,8 +78,10 @@ class RackTypeTestCase(TestCase): @classmethod def setUpTestData(cls): + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') RackType.objects.create( + manufacturer=manufacturer, name='RackType 1', slug='rack-type-1', width=11, diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index b68c51450..41664995b 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -341,17 +341,23 @@ class RackTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase): @classmethod def setUpTestData(cls): - - racks = ( - RackType(name='RackType 1', slug='rack-type-1',), - RackType(name='RackType 2', slug='rack-type-2',), - RackType(name='RackType 3', slug='rack-type-3',), + manufacturers = ( + Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), + Manufacturer(name='Manufacturer 2', slug='manufacturer-2'), ) - RackType.objects.bulk_create(racks) + Manufacturer.objects.bulk_create(manufacturers) + + rack_types = ( + RackType(manufacturer=manufacturers[0], name='RackType 1', slug='rack-type-1',), + RackType(manufacturer=manufacturers[0], name='RackType 2', slug='rack-type-2',), + RackType(manufacturer=manufacturers[0], name='RackType 3', slug='rack-type-3',), + ) + RackType.objects.bulk_create(rack_types) tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { + 'manufacturer': manufacturers[1].pk, 'name': 'RackType X', 'slug': 'rack-type-x', 'type': RackTypeChoices.TYPE_CABINET, @@ -370,20 +376,21 @@ class RackTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase): } cls.csv_data = ( - "name,slug,width,u_height,weight,max_weight,weight_unit", - "RackType 4,rack-type-4,19,42,100,2000,kg", - "RackType 5,rack-type-5,19,42,100,2000,kg", - "RackType 6,rack-type-6,19,42,100,2000,kg", + "manufacturer,name,slug,width,u_height,weight,max_weight,weight_unit", + "Manufacturer 1,RackType 4,rack-type-4,19,42,100,2000,kg", + "Manufacturer 1,RackType 5,rack-type-5,19,42,100,2000,kg", + "Manufacturer 1,RackType 6,rack-type-6,19,42,100,2000,kg", ) cls.csv_update_data = ( "id,name", - f"{racks[0].pk},RackType 7", - f"{racks[1].pk},RackType 8", - f"{racks[2].pk},RackType 9", + f"{rack_types[0].pk},RackType 7", + f"{rack_types[1].pk},RackType 8", + f"{rack_types[2].pk},RackType 9", ) cls.bulk_edit_data = { + 'manufacturer': manufacturers[1].pk, 'type': RackTypeChoices.TYPE_4POST, 'width': RackWidthChoices.WIDTH_23IN, 'u_height': 49, diff --git a/netbox/templates/dcim/racktype.html b/netbox/templates/dcim/racktype.html index e8d50cac8..7d87afaf4 100644 --- a/netbox/templates/dcim/racktype.html +++ b/netbox/templates/dcim/racktype.html @@ -12,6 +12,10 @@
{% trans "Rack Type" %}
+ + + +
{% trans "Manufacturer" %}{{ object.manufacturer|linkify }}
{% trans "Name" %} {{ object.name }}