Add manufacturer field to RackType

This commit is contained in:
Jeremy Stretch 2024-07-14 16:33:30 -04:00
parent a54655cc94
commit 2bde82e37b
16 changed files with 169 additions and 81 deletions

View File

@ -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.

View File

@ -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):

View File

@ -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):

View File

@ -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')),
)

View File

@ -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):

View File

@ -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,

View File

@ -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',
]

View File

@ -614,6 +614,7 @@ class PowerPortTemplateType(ModularComponentTemplateType):
)
class RackTypeType(NetBoxObjectType):
_name: str
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
@strawberry_django.type(

View File

@ -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)),

View File

@ -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.

View File

@ -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',
)

View File

@ -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',
},
]

View File

@ -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)

View File

@ -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,

View File

@ -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,

View File

@ -12,6 +12,10 @@
<div class="card">
<h5 class="card-header">{% trans "Rack Type" %}</h5>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Manufacturer" %}</th>
<td>{{ object.manufacturer|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>