mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-05 11:46:50 -06:00
12826 Add Rack Type (#16739)
* 12826 add RackType * 12826 add forms, filters, tables * 12826 add to menu * 12826 remove role * 12826 add api/serializers * 12826 add tests and fixes * 12826 fix tests * 12826 fix tests * 12826 fix tests * 12826 fix tests * 12826 add device_type to device and instantiation * 12826 test device creation * 12826 add slug * 12826 fix tests * 12826 fix slug field * 12826 prevent modification of rack fields if rack_type set * 12826 update rack fields on rack_type edit * Misc cleanup * Update model docs * Add manufacturer field to RackType * Add test for mounting_depth * Rename 'type' to 'form_factor' * Create base classes for Rack & RackType models, serializers * Hide RackType-defined fields on RackForm when a rack type is set * Establish a base filter form for Rack & RackType * Clean up RackType attr inheritance * Clean up templates --------- Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
This commit is contained in:
@@ -3,12 +3,13 @@ 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
|
||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||
from users.api.serializers_.users import UserSerializer
|
||||
from .manufacturers import ManufacturerSerializer
|
||||
from .sites import LocationSerializer, SiteSerializer
|
||||
|
||||
__all__ = (
|
||||
@@ -16,6 +17,7 @@ __all__ = (
|
||||
'RackReservationSerializer',
|
||||
'RackRoleSerializer',
|
||||
'RackSerializer',
|
||||
'RackTypeSerializer',
|
||||
)
|
||||
|
||||
|
||||
@@ -33,18 +35,84 @@ class RackRoleSerializer(NetBoxModelSerializer):
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count')
|
||||
|
||||
|
||||
class RackSerializer(NetBoxModelSerializer):
|
||||
site = SiteSerializer(nested=True)
|
||||
location = LocationSerializer(nested=True, required=False, allow_null=True, default=None)
|
||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||
status = ChoiceField(choices=RackStatusChoices, required=False)
|
||||
role = RackRoleSerializer(nested=True, required=False, allow_null=True)
|
||||
type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False, allow_null=True)
|
||||
facility_id = serializers.CharField(max_length=50, allow_blank=True, allow_null=True, label=_('Facility ID'),
|
||||
default=None)
|
||||
width = ChoiceField(choices=RackWidthChoices, required=False)
|
||||
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||
class RackBaseSerializer(NetBoxModelSerializer):
|
||||
form_factor = ChoiceField(
|
||||
choices=RackFormFactorChoices,
|
||||
allow_blank=True,
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
width = ChoiceField(
|
||||
choices=RackWidthChoices,
|
||||
required=False
|
||||
)
|
||||
outer_unit = ChoiceField(
|
||||
choices=RackDimensionUnitChoices,
|
||||
allow_blank=True,
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
weight_unit = ChoiceField(
|
||||
choices=WeightUnitChoices,
|
||||
allow_blank=True,
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
|
||||
|
||||
class RackTypeSerializer(RackBaseSerializer):
|
||||
manufacturer = ManufacturerSerializer(
|
||||
nested=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RackType
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'manufacturer', 'name', 'slug', 'description', 'form_factor',
|
||||
'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', 'manufacturer', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
class RackSerializer(RackBaseSerializer):
|
||||
site = SiteSerializer(
|
||||
nested=True
|
||||
)
|
||||
location = LocationSerializer(
|
||||
nested=True,
|
||||
required=False,
|
||||
allow_null=True,
|
||||
default=None
|
||||
)
|
||||
tenant = TenantSerializer(
|
||||
nested=True,
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
status = ChoiceField(
|
||||
choices=RackStatusChoices,
|
||||
required=False
|
||||
)
|
||||
role = RackRoleSerializer(
|
||||
nested=True,
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
facility_id = serializers.CharField(
|
||||
max_length=50,
|
||||
allow_blank=True,
|
||||
allow_null=True,
|
||||
label=_('Facility ID'),
|
||||
default=None
|
||||
)
|
||||
rack_type = RackTypeSerializer(
|
||||
nested=True,
|
||||
required=False,
|
||||
allow_null=True,
|
||||
default=None
|
||||
)
|
||||
|
||||
# Related object counts
|
||||
device_count = RelatedObjectCountField('devices')
|
||||
@@ -54,9 +122,10 @@ class RackSerializer(NetBoxModelSerializer):
|
||||
model = Rack
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status',
|
||||
'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'starting_unit', 'weight', 'max_weight',
|
||||
'weight_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'description',
|
||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count',
|
||||
'role', 'serial', 'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'weight',
|
||||
'max_weight', 'weight_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth',
|
||||
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
|
||||
'powerfeed_count',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count')
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ router.register('sites', views.SiteViewSet)
|
||||
|
||||
# Racks
|
||||
router.register('locations', views.LocationViewSet)
|
||||
router.register('rack-types', views.RackTypeViewSet)
|
||||
router.register('rack-roles', views.RackRoleViewSet)
|
||||
router.register('racks', views.RackViewSet)
|
||||
router.register('rack-reservations', views.RackReservationViewSet)
|
||||
|
||||
@@ -161,6 +161,16 @@ class RackRoleViewSet(NetBoxModelViewSet):
|
||||
filterset_class = filtersets.RackRoleFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Rack Types
|
||||
#
|
||||
|
||||
class RackTypeViewSet(NetBoxModelViewSet):
|
||||
queryset = RackType.objects.all()
|
||||
serializer_class = serializers.RackTypeSerializer
|
||||
filterset_class = filtersets.RackTypeFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
|
||||
@@ -51,7 +51,7 @@ class LocationStatusChoices(ChoiceSet):
|
||||
# Racks
|
||||
#
|
||||
|
||||
class RackTypeChoices(ChoiceSet):
|
||||
class RackFormFactorChoices(ChoiceSet):
|
||||
|
||||
TYPE_2POST = '2-post-frame'
|
||||
TYPE_4POST = '4-post-frame'
|
||||
|
||||
@@ -69,6 +69,7 @@ __all__ = (
|
||||
'RackFilterSet',
|
||||
'RackReservationFilterSet',
|
||||
'RackRoleFilterSet',
|
||||
'RackTypeFilterSet',
|
||||
'RearPortFilterSet',
|
||||
'RearPortTemplateFilterSet',
|
||||
'RegionFilterSet',
|
||||
@@ -289,6 +290,41 @@ class RackRoleFilterSet(OrganizationalModelFilterSet):
|
||||
fields = ('id', 'name', 'slug', 'color', 'description')
|
||||
|
||||
|
||||
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)'),
|
||||
)
|
||||
form_factor = django_filters.MultipleChoiceFilter(
|
||||
choices=RackFormFactorChoices
|
||||
)
|
||||
width = django_filters.MultipleChoiceFilter(
|
||||
choices=RackWidthChoices
|
||||
)
|
||||
|
||||
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',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
@@ -339,12 +375,22 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
|
||||
to_field_name='slug',
|
||||
label=_('Location (slug)'),
|
||||
)
|
||||
rack_type = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='rack_type__slug',
|
||||
queryset=RackType.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Rack type (slug)'),
|
||||
)
|
||||
rack_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=RackType.objects.all(),
|
||||
label=_('Rack type (ID)'),
|
||||
)
|
||||
status = django_filters.MultipleChoiceFilter(
|
||||
choices=RackStatusChoices,
|
||||
null_value=None
|
||||
)
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=RackTypeChoices
|
||||
form_factor = django_filters.MultipleChoiceFilter(
|
||||
choices=RackFormFactorChoices
|
||||
)
|
||||
width = django_filters.MultipleChoiceFilter(
|
||||
choices=RackWidthChoices
|
||||
|
||||
@@ -13,7 +13,7 @@ from netbox.forms import NetBoxModelBulkEditForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
|
||||
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.rendering import FieldSet, InlineFields
|
||||
from utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions
|
||||
from wireless.models import WirelessLAN, WirelessLANGroup
|
||||
from wireless.choices import WirelessRoleChoices
|
||||
@@ -52,6 +52,7 @@ __all__ = (
|
||||
'RackBulkEditForm',
|
||||
'RackReservationBulkEditForm',
|
||||
'RackRoleBulkEditForm',
|
||||
'RackTypeBulkEditForm',
|
||||
'RearPortBulkEditForm',
|
||||
'RearPortTemplateBulkEditForm',
|
||||
'RegionBulkEditForm',
|
||||
@@ -218,6 +219,97 @@ class RackRoleBulkEditForm(NetBoxModelBulkEditForm):
|
||||
nullable_fields = ('color', 'description')
|
||||
|
||||
|
||||
class RackTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
label=_('Manufacturer'),
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False
|
||||
)
|
||||
form_factor = forms.ChoiceField(
|
||||
label=_('Form factor'),
|
||||
choices=add_blank_choice(RackFormFactorChoices),
|
||||
required=False
|
||||
)
|
||||
width = forms.ChoiceField(
|
||||
label=_('Width'),
|
||||
choices=add_blank_choice(RackWidthChoices),
|
||||
required=False
|
||||
)
|
||||
u_height = forms.IntegerField(
|
||||
required=False,
|
||||
label=_('Height (U)')
|
||||
)
|
||||
starting_unit = forms.IntegerField(
|
||||
required=False,
|
||||
min_value=1
|
||||
)
|
||||
desc_units = forms.NullBooleanField(
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect,
|
||||
label=_('Descending units')
|
||||
)
|
||||
outer_width = forms.IntegerField(
|
||||
label=_('Outer width'),
|
||||
required=False,
|
||||
min_value=1
|
||||
)
|
||||
outer_depth = forms.IntegerField(
|
||||
label=_('Outer depth'),
|
||||
required=False,
|
||||
min_value=1
|
||||
)
|
||||
outer_unit = forms.ChoiceField(
|
||||
label=_('Outer unit'),
|
||||
choices=add_blank_choice(RackDimensionUnitChoices),
|
||||
required=False
|
||||
)
|
||||
mounting_depth = forms.IntegerField(
|
||||
label=_('Mounting depth'),
|
||||
required=False,
|
||||
min_value=1
|
||||
)
|
||||
weight = forms.DecimalField(
|
||||
label=_('Weight'),
|
||||
min_value=0,
|
||||
required=False
|
||||
)
|
||||
max_weight = forms.IntegerField(
|
||||
label=_('Max weight'),
|
||||
min_value=0,
|
||||
required=False
|
||||
)
|
||||
weight_unit = forms.ChoiceField(
|
||||
label=_('Weight unit'),
|
||||
choices=add_blank_choice(WeightUnitChoices),
|
||||
required=False,
|
||||
initial=''
|
||||
)
|
||||
description = forms.CharField(
|
||||
label=_('Description'),
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
model = RackType
|
||||
fieldsets = (
|
||||
FieldSet('manufacturer', 'description', 'form_factor', name=_('Rack Type')),
|
||||
FieldSet(
|
||||
'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')
|
||||
),
|
||||
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'outer_width', 'outer_depth', 'outer_unit', 'weight',
|
||||
'max_weight', 'weight_unit', 'description', 'comments',
|
||||
)
|
||||
|
||||
|
||||
class RackBulkEditForm(NetBoxModelBulkEditForm):
|
||||
region = DynamicModelChoiceField(
|
||||
label=_('Region'),
|
||||
@@ -278,9 +370,9 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
|
||||
max_length=50,
|
||||
required=False
|
||||
)
|
||||
type = forms.ChoiceField(
|
||||
label=_('Type'),
|
||||
choices=add_blank_choice(RackTypeChoices),
|
||||
form_factor = forms.ChoiceField(
|
||||
label=_('Form factor'),
|
||||
choices=add_blank_choice(RackFormFactorChoices),
|
||||
required=False
|
||||
)
|
||||
width = forms.ChoiceField(
|
||||
@@ -345,8 +437,8 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
|
||||
FieldSet('status', 'role', 'tenant', 'serial', 'asset_tag', 'description', name=_('Rack')),
|
||||
FieldSet('region', 'site_group', 'site', 'location', name=_('Location')),
|
||||
FieldSet(
|
||||
'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth',
|
||||
name=_('Hardware')
|
||||
'form_factor', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
|
||||
'mounting_depth', name=_('Hardware')
|
||||
),
|
||||
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
||||
)
|
||||
|
||||
@@ -45,6 +45,7 @@ __all__ = (
|
||||
'RackImportForm',
|
||||
'RackReservationImportForm',
|
||||
'RackRoleImportForm',
|
||||
'RackTypeImportForm',
|
||||
'RearPortImportForm',
|
||||
'RegionImportForm',
|
||||
'SiteImportForm',
|
||||
@@ -176,6 +177,54 @@ class RackRoleImportForm(NetBoxModelImportForm):
|
||||
fields = ('name', 'slug', 'color', 'description', 'tags')
|
||||
|
||||
|
||||
class RackTypeImportForm(NetBoxModelImportForm):
|
||||
manufacturer = forms.ModelChoiceField(
|
||||
label=_('Manufacturer'),
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text=_('The manufacturer of this rack type')
|
||||
)
|
||||
form_factor = CSVChoiceField(
|
||||
label=_('Type'),
|
||||
choices=RackFormFactorChoices,
|
||||
required=False,
|
||||
help_text=_('Form factor')
|
||||
)
|
||||
starting_unit = forms.IntegerField(
|
||||
required=False,
|
||||
min_value=1,
|
||||
help_text=_('The lowest-numbered position in the rack')
|
||||
)
|
||||
width = forms.ChoiceField(
|
||||
label=_('Width'),
|
||||
choices=RackWidthChoices,
|
||||
help_text=_('Rail-to-rail width (in inches)')
|
||||
)
|
||||
outer_unit = CSVChoiceField(
|
||||
label=_('Outer unit'),
|
||||
choices=RackDimensionUnitChoices,
|
||||
required=False,
|
||||
help_text=_('Unit for outer dimensions')
|
||||
)
|
||||
weight_unit = CSVChoiceField(
|
||||
label=_('Weight unit'),
|
||||
choices=WeightUnitChoices,
|
||||
required=False,
|
||||
help_text=_('Unit for rack weights')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RackType
|
||||
fields = (
|
||||
'manufacturer', 'name', 'slug', 'form_factor', '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):
|
||||
super().__init__(data, *args, **kwargs)
|
||||
|
||||
|
||||
class RackImportForm(NetBoxModelImportForm):
|
||||
site = CSVModelChoiceField(
|
||||
label=_('Site'),
|
||||
@@ -207,11 +256,11 @@ class RackImportForm(NetBoxModelImportForm):
|
||||
to_field_name='name',
|
||||
help_text=_('Name of assigned role')
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
form_factor = CSVChoiceField(
|
||||
label=_('Type'),
|
||||
choices=RackTypeChoices,
|
||||
choices=RackFormFactorChoices,
|
||||
required=False,
|
||||
help_text=_('Rack type')
|
||||
help_text=_('Form factor')
|
||||
)
|
||||
width = forms.ChoiceField(
|
||||
label=_('Width'),
|
||||
@@ -234,7 +283,7 @@ class RackImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = (
|
||||
'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'type', 'serial', 'asset_tag',
|
||||
'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'form_factor', 'serial', 'asset_tag',
|
||||
'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight',
|
||||
'max_weight', 'weight_unit', 'description', 'comments', 'tags',
|
||||
)
|
||||
|
||||
@@ -48,6 +48,7 @@ __all__ = (
|
||||
'RackElevationFilterForm',
|
||||
'RackReservationFilterForm',
|
||||
'RackRoleFilterForm',
|
||||
'RackTypeFilterForm',
|
||||
'RearPortFilterForm',
|
||||
'RegionFilterForm',
|
||||
'SiteFilterForm',
|
||||
@@ -240,16 +241,77 @@ class RackRoleFilterForm(NetBoxModelFilterSetForm):
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
class RackBaseFilterForm(NetBoxModelFilterSetForm):
|
||||
form_factor = forms.MultipleChoiceField(
|
||||
label=_('Form factor'),
|
||||
choices=RackFormFactorChoices,
|
||||
required=False
|
||||
)
|
||||
width = forms.MultipleChoiceField(
|
||||
label=_('Width'),
|
||||
choices=RackWidthChoices,
|
||||
required=False
|
||||
)
|
||||
u_height = forms.IntegerField(
|
||||
required=False,
|
||||
min_value=1
|
||||
)
|
||||
starting_unit = forms.IntegerField(
|
||||
required=False,
|
||||
min_value=1
|
||||
)
|
||||
desc_units = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_('Descending units'),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
weight = forms.DecimalField(
|
||||
label=_('Weight'),
|
||||
required=False,
|
||||
min_value=1
|
||||
)
|
||||
max_weight = forms.IntegerField(
|
||||
label=_('Max weight'),
|
||||
required=False,
|
||||
min_value=1
|
||||
)
|
||||
weight_unit = forms.ChoiceField(
|
||||
label=_('Weight unit'),
|
||||
choices=add_blank_choice(WeightUnitChoices),
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
class RackTypeFilterForm(RackBaseFilterForm):
|
||||
model = RackType
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('form_factor', 'width', 'u_height', name=_('Rack Type')),
|
||||
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
|
||||
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'manufacturer_id')
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
label=_('Manufacturer')
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, RackBaseFilterForm):
|
||||
model = Rack
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
||||
FieldSet('status', 'role_id', name=_('Function')),
|
||||
FieldSet('type', 'width', 'serial', 'asset_tag', name=_('Hardware')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
FieldSet('status', 'role_id', 'serial', 'asset_tag', name=_('Rack')),
|
||||
FieldSet('form_factor', 'width', 'u_height', name=_('Rack Type')),
|
||||
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
|
||||
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id')
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
@@ -284,16 +346,6 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
|
||||
choices=RackStatusChoices,
|
||||
required=False
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
label=_('Type'),
|
||||
choices=RackTypeChoices,
|
||||
required=False
|
||||
)
|
||||
width = forms.MultipleChoiceField(
|
||||
label=_('Width'),
|
||||
choices=RackWidthChoices,
|
||||
required=False
|
||||
)
|
||||
role_id = DynamicModelMultipleChoiceField(
|
||||
queryset=RackRole.objects.all(),
|
||||
required=False,
|
||||
@@ -309,21 +361,6 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
weight = forms.DecimalField(
|
||||
label=_('Weight'),
|
||||
required=False,
|
||||
min_value=1
|
||||
)
|
||||
max_weight = forms.IntegerField(
|
||||
label=_('Max weight'),
|
||||
required=False,
|
||||
min_value=1
|
||||
)
|
||||
weight_unit = forms.ChoiceField(
|
||||
label=_('Weight unit'),
|
||||
choices=add_blank_choice(WeightUnitChoices),
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
class RackElevationFilterForm(RackFilterForm):
|
||||
|
||||
@@ -11,7 +11,7 @@ from extras.models import ConfigTemplate
|
||||
from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from tenancy.forms import TenancyForm
|
||||
from utilities.forms import add_blank_choice
|
||||
from utilities.forms import add_blank_choice, get_field_value
|
||||
from utilities.forms.fields import (
|
||||
CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField,
|
||||
)
|
||||
@@ -57,6 +57,7 @@ __all__ = (
|
||||
'RackForm',
|
||||
'RackReservationForm',
|
||||
'RackRoleForm',
|
||||
'RackTypeForm',
|
||||
'RearPortForm',
|
||||
'RearPortTemplateForm',
|
||||
'RegionForm',
|
||||
@@ -201,6 +202,34 @@ class RackRoleForm(NetBoxModelForm):
|
||||
]
|
||||
|
||||
|
||||
class RackTypeForm(NetBoxModelForm):
|
||||
manufacturer = DynamicModelChoiceField(
|
||||
label=_('Manufacturer'),
|
||||
queryset=Manufacturer.objects.all()
|
||||
)
|
||||
comments = CommentField()
|
||||
slug = SlugField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('manufacturer', 'name', 'slug', 'description', 'form_factor', 'tags', name=_('Rack Type')),
|
||||
FieldSet(
|
||||
'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')
|
||||
),
|
||||
FieldSet('starting_unit', 'desc_units', name=_('Numbering')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RackType
|
||||
fields = [
|
||||
'manufacturer', 'name', 'slug', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units',
|
||||
'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
|
||||
'description', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class RackForm(TenancyForm, NetBoxModelForm):
|
||||
site = DynamicModelChoiceField(
|
||||
label=_('Site'),
|
||||
@@ -220,28 +249,54 @@ class RackForm(TenancyForm, NetBoxModelForm):
|
||||
queryset=RackRole.objects.all(),
|
||||
required=False
|
||||
)
|
||||
rack_type = DynamicModelChoiceField(
|
||||
label=_('Rack Type'),
|
||||
queryset=RackType.objects.all(),
|
||||
required=False,
|
||||
help_text=_("Select a pre-defined rack type, or set physical characteristics below.")
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('site', 'location', 'name', 'status', 'role', 'description', 'tags', name=_('Rack')),
|
||||
FieldSet('site', 'location', 'name', 'status', 'role', 'rack_type', 'description', 'tags', name=_('Rack')),
|
||||
FieldSet('facility_id', 'serial', 'asset_tag', name=_('Inventory Control')),
|
||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||
FieldSet(
|
||||
'type', 'width', 'starting_unit', 'u_height',
|
||||
InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
|
||||
InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
|
||||
'mounting_depth', 'desc_units', name=_('Dimensions')
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = [
|
||||
'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
|
||||
'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',
|
||||
'asset_tag', 'rack_type', 'form_factor', '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, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Mimic HTMXSelect()
|
||||
self.fields['rack_type'].widget.attrs.update({
|
||||
'hx-get': '.',
|
||||
'hx-include': '#form_fields',
|
||||
'hx-target': '#form_fields',
|
||||
})
|
||||
|
||||
# Omit RackType-defined fields if rack_type is set
|
||||
if get_field_value(self, 'rack_type'):
|
||||
for field_name in Rack.RACKTYPE_FIELDS:
|
||||
del self.fields[field_name]
|
||||
else:
|
||||
self.fieldsets = (
|
||||
*self.fieldsets,
|
||||
FieldSet(
|
||||
'form_factor', 'width', 'starting_unit', 'u_height',
|
||||
InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
|
||||
InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
|
||||
'mounting_depth', 'desc_units', name=_('Dimensions')
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class RackReservationForm(TenancyForm, NetBoxModelForm):
|
||||
rack = DynamicModelChoiceField(
|
||||
|
||||
@@ -38,6 +38,7 @@ __all__ = (
|
||||
'RackFilter',
|
||||
'RackReservationFilter',
|
||||
'RackRoleFilter',
|
||||
'RackTypeFilter',
|
||||
'RearPortFilter',
|
||||
'RearPortTemplateFilter',
|
||||
'RegionFilter',
|
||||
@@ -234,6 +235,12 @@ class PowerPortTemplateFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.RackType, lookups=True)
|
||||
@autotype_decorator(filtersets.RackTypeFilterSet)
|
||||
class RackTypeFilter(BaseFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Rack, lookups=True)
|
||||
@autotype_decorator(filtersets.RackFilterSet)
|
||||
class RackFilter(BaseFilterMixin):
|
||||
|
||||
@@ -159,6 +159,11 @@ class DCIMQuery:
|
||||
return models.PowerPortTemplate.objects.get(id=id)
|
||||
power_port_template_list: List[PowerPortTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rack_type(self, id: int) -> RackTypeType:
|
||||
return models.RackType.objects.get(id=id)
|
||||
rack_type_list: List[RackTypeType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rack(self, id: int) -> RackType:
|
||||
return models.Rack.objects.get(id=id)
|
||||
|
||||
@@ -50,6 +50,7 @@ __all__ = (
|
||||
'RackType',
|
||||
'RackReservationType',
|
||||
'RackRoleType',
|
||||
'RackTypeType',
|
||||
'RearPortType',
|
||||
'RearPortTemplateType',
|
||||
'RegionType',
|
||||
@@ -606,6 +607,16 @@ class PowerPortTemplateType(ModularComponentTemplateType):
|
||||
poweroutlet_templates: List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.RackType,
|
||||
fields='__all__',
|
||||
filters=RackTypeFilter
|
||||
)
|
||||
class RackTypeType(NetBoxObjectType):
|
||||
_name: str
|
||||
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.Rack,
|
||||
fields='__all__',
|
||||
@@ -618,6 +629,7 @@ class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje
|
||||
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
|
||||
role: Annotated["RackRoleType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
|
||||
rack_type: Annotated["RackTypeType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
reservations: List[Annotated["RackReservationType", strawberry.lazy('dcim.graphql.types')]]
|
||||
devices: List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]
|
||||
powerfeeds: List[Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')]]
|
||||
|
||||
93
netbox/dcim/migrations/0188_racktype.py
Normal file
93
netbox/dcim/migrations/0188_racktype.py
Normal file
@@ -0,0 +1,93 @@
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
from django.db import migrations, models
|
||||
|
||||
import utilities.fields
|
||||
import utilities.json
|
||||
import utilities.ordering
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0117_customfield_uniqueness'),
|
||||
('dcim', '0187_alter_device_vc_position'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='RackType',
|
||||
fields=[
|
||||
('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
|
||||
)),
|
||||
('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
|
||||
),
|
||||
),
|
||||
('slug', models.SlugField(max_length=100, unique=True)),
|
||||
('form_factor', 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)]
|
||||
)),
|
||||
('desc_units', models.BooleanField(default=False)),
|
||||
('outer_width', models.PositiveSmallIntegerField(blank=True, null=True)),
|
||||
('outer_depth', models.PositiveSmallIntegerField(blank=True, null=True)),
|
||||
('outer_unit', models.CharField(blank=True, max_length=50)),
|
||||
('max_weight', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('_abs_max_weight', models.PositiveBigIntegerField(blank=True, null=True)),
|
||||
('mounting_depth', models.PositiveSmallIntegerField(blank=True, null=True)),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'racktype',
|
||||
'verbose_name_plural': 'racktypes',
|
||||
'ordering': ('_name', 'pk'),
|
||||
},
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='rack',
|
||||
old_name='type',
|
||||
new_name='form_factor',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rack',
|
||||
name='rack_type',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='racks',
|
||||
to='dcim.racktype',
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -29,9 +29,181 @@ __all__ = (
|
||||
'Rack',
|
||||
'RackReservation',
|
||||
'RackRole',
|
||||
'RackType',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Rack Types
|
||||
#
|
||||
|
||||
class RackBase(WeightMixin, PrimaryModel):
|
||||
"""
|
||||
Base class for RackType & Rack. Holds
|
||||
"""
|
||||
form_factor = models.CharField(
|
||||
choices=RackFormFactorChoices,
|
||||
max_length=50,
|
||||
blank=True,
|
||||
verbose_name=_('form factor')
|
||||
)
|
||||
width = models.PositiveSmallIntegerField(
|
||||
choices=RackWidthChoices,
|
||||
default=RackWidthChoices.WIDTH_19IN,
|
||||
verbose_name=_('width'),
|
||||
help_text=_('Rail-to-rail width')
|
||||
)
|
||||
|
||||
# Numbering
|
||||
u_height = models.PositiveSmallIntegerField(
|
||||
default=RACK_U_HEIGHT_DEFAULT,
|
||||
verbose_name=_('height (U)'),
|
||||
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'),
|
||||
validators=[MinValueValidator(1)],
|
||||
help_text=_('Starting unit for rack')
|
||||
)
|
||||
desc_units = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('descending units'),
|
||||
help_text=_('Units are numbered top-to-bottom')
|
||||
)
|
||||
|
||||
# Dimensions
|
||||
outer_width = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('outer width'),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('Outer dimension of rack (width)')
|
||||
)
|
||||
outer_depth = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('outer depth'),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('Outer dimension of rack (depth)')
|
||||
)
|
||||
outer_unit = models.CharField(
|
||||
verbose_name=_('outer unit'),
|
||||
max_length=50,
|
||||
choices=RackDimensionUnitChoices,
|
||||
blank=True
|
||||
)
|
||||
mounting_depth = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('mounting depth'),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=(_(
|
||||
'Maximum depth of a mounted device, in millimeters. For four-post racks, this is the distance between the '
|
||||
'front and rear rails.'
|
||||
))
|
||||
)
|
||||
|
||||
# Weight
|
||||
# WeightMixin provides weight, weight_unit, and _abs_weight
|
||||
max_weight = models.PositiveIntegerField(
|
||||
verbose_name=_('max weight'),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('Maximum load capacity for the rack')
|
||||
)
|
||||
# Stores the normalized max weight (in grams) for database ordering
|
||||
_abs_max_weight = models.PositiveBigIntegerField(
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class RackType(RackBase):
|
||||
"""
|
||||
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
|
||||
Each Rack is assigned to a Site and (optionally) a Location.
|
||||
"""
|
||||
manufacturer = models.ForeignKey(
|
||||
to='dcim.Manufacturer',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='rack_types'
|
||||
)
|
||||
name = models.CharField(
|
||||
verbose_name=_('name'),
|
||||
max_length=100
|
||||
)
|
||||
_name = NaturalOrderingField(
|
||||
target_field='name',
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
slug = models.SlugField(
|
||||
verbose_name=_('slug'),
|
||||
max_length=100,
|
||||
unique=True
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'manufacturer', 'form_factor', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
|
||||
'mounting_depth', 'weight', 'max_weight', 'weight_unit',
|
||||
)
|
||||
prerequisite_models = (
|
||||
'dcim.Manufacturer',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('_name', 'pk') # (site, location, name) may be non-unique
|
||||
verbose_name = _('rack type')
|
||||
verbose_name_plural = _('rack types')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('dcim:racktype', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate outer dimensions and unit
|
||||
if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_unit:
|
||||
raise ValidationError(_("Must specify a unit when setting an outer width/depth"))
|
||||
|
||||
# Validate max_weight and weight_unit
|
||||
if self.max_weight and not self.weight_unit:
|
||||
raise ValidationError(_("Must specify a unit when setting a maximum weight"))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Store the given max weight (if any) in grams for use in database ordering
|
||||
if self.max_weight and self.weight_unit:
|
||||
self._abs_max_weight = to_grams(self.max_weight, self.weight_unit)
|
||||
else:
|
||||
self._abs_max_weight = None
|
||||
|
||||
# Clear unit if outer width & depth are not set
|
||||
if self.outer_width is None and self.outer_depth is None:
|
||||
self.outer_unit = ''
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# Update all Racks associated with this RackType
|
||||
for rack in self.racks.all():
|
||||
rack.snapshot()
|
||||
rack.copy_racktype_attrs()
|
||||
rack.save()
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
"""
|
||||
Return a list of unit numbers, top to bottom.
|
||||
"""
|
||||
if self.desc_units:
|
||||
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)
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
@@ -54,11 +226,24 @@ class RackRole(OrganizationalModel):
|
||||
return reverse('dcim:rackrole', args=[self.pk])
|
||||
|
||||
|
||||
class Rack(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
||||
class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
|
||||
"""
|
||||
Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
|
||||
Each Rack is assigned to a Site and (optionally) a Location.
|
||||
"""
|
||||
# Fields which cannot be set locally if a RackType is assigned
|
||||
RACKTYPE_FIELDS = [
|
||||
'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
|
||||
'mounting_depth', 'weight', 'weight_unit', 'max_weight'
|
||||
]
|
||||
|
||||
rack_type = models.ForeignKey(
|
||||
to='dcim.RackType',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='racks',
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
name = models.CharField(
|
||||
verbose_name=_('name'),
|
||||
max_length=100
|
||||
@@ -121,73 +306,6 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
||||
verbose_name=_('asset tag'),
|
||||
help_text=_('A unique tag used to identify this rack')
|
||||
)
|
||||
type = models.CharField(
|
||||
choices=RackTypeChoices,
|
||||
max_length=50,
|
||||
blank=True,
|
||||
verbose_name=_('type')
|
||||
)
|
||||
width = models.PositiveSmallIntegerField(
|
||||
choices=RackWidthChoices,
|
||||
default=RackWidthChoices.WIDTH_19IN,
|
||||
verbose_name=_('width'),
|
||||
help_text=_('Rail-to-rail width')
|
||||
)
|
||||
u_height = models.PositiveSmallIntegerField(
|
||||
default=RACK_U_HEIGHT_DEFAULT,
|
||||
verbose_name=_('height (U)'),
|
||||
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'),
|
||||
validators=[MinValueValidator(1),],
|
||||
help_text=_('Starting unit for rack')
|
||||
)
|
||||
desc_units = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('descending units'),
|
||||
help_text=_('Units are numbered top-to-bottom')
|
||||
)
|
||||
outer_width = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('outer width'),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('Outer dimension of rack (width)')
|
||||
)
|
||||
outer_depth = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('outer depth'),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('Outer dimension of rack (depth)')
|
||||
)
|
||||
outer_unit = models.CharField(
|
||||
verbose_name=_('outer unit'),
|
||||
max_length=50,
|
||||
choices=RackDimensionUnitChoices,
|
||||
blank=True,
|
||||
)
|
||||
max_weight = models.PositiveIntegerField(
|
||||
verbose_name=_('max weight'),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_('Maximum load capacity for the rack')
|
||||
)
|
||||
# Stores the normalized max weight (in grams) for database ordering
|
||||
_abs_max_weight = models.PositiveBigIntegerField(
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
mounting_depth = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('mounting depth'),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=(
|
||||
_('Maximum depth of a mounted device, in millimeters. For four-post racks, this is the '
|
||||
'distance between the front and rear rails.')
|
||||
)
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
vlan_groups = GenericRelation(
|
||||
@@ -198,7 +316,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'site', 'location', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
|
||||
'site', 'location', 'tenant', 'status', 'role', 'form_factor', 'width', 'u_height', 'desc_units', 'outer_width',
|
||||
'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
|
||||
)
|
||||
prerequisite_models = (
|
||||
@@ -271,6 +389,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.copy_racktype_attrs()
|
||||
|
||||
# Store the given max weight (if any) in grams for use in database ordering
|
||||
if self.max_weight and self.weight_unit:
|
||||
@@ -284,6 +403,14 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def copy_racktype_attrs(self):
|
||||
"""
|
||||
Copy physical attributes from the assigned RackType (if any).
|
||||
"""
|
||||
if self.rack_type:
|
||||
for field_name in self.RACKTYPE_FIELDS:
|
||||
setattr(self, field_name, getattr(self.rack_type, field_name))
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
"""
|
||||
|
||||
@@ -242,6 +242,17 @@ 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 = ('type', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
class RackIndex(SearchIndex):
|
||||
model = models.Rack
|
||||
|
||||
@@ -2,7 +2,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
import django_tables2 as tables
|
||||
from django_tables2.utils import Accessor
|
||||
|
||||
from dcim.models import Rack, RackReservation, RackRole
|
||||
from dcim.models import Rack, RackReservation, RackRole, RackType
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||
from .template_code import WEIGHT
|
||||
@@ -11,6 +11,7 @@ __all__ = (
|
||||
'RackTable',
|
||||
'RackReservationTable',
|
||||
'RackRoleTable',
|
||||
'RackTypeTable',
|
||||
)
|
||||
|
||||
|
||||
@@ -44,6 +45,61 @@ class RackRoleTable(NetBoxTable):
|
||||
default_columns = ('pk', 'name', 'rack_count', 'color', 'description')
|
||||
|
||||
|
||||
#
|
||||
# Rack Types
|
||||
#
|
||||
|
||||
class RackTypeTable(NetBoxTable):
|
||||
name = tables.Column(
|
||||
verbose_name=_('Name'),
|
||||
order_by=('_name',),
|
||||
linkify=True
|
||||
)
|
||||
manufacturer = tables.Column(
|
||||
verbose_name=_('Manufacturer'),
|
||||
linkify=True
|
||||
)
|
||||
u_height = tables.TemplateColumn(
|
||||
template_code="{{ value }}U",
|
||||
verbose_name=_('Height')
|
||||
)
|
||||
outer_width = tables.TemplateColumn(
|
||||
template_code="{{ record.outer_width }} {{ record.outer_unit }}",
|
||||
verbose_name=_('Outer Width')
|
||||
)
|
||||
outer_depth = tables.TemplateColumn(
|
||||
template_code="{{ record.outer_depth }} {{ record.outer_unit }}",
|
||||
verbose_name=_('Outer Depth')
|
||||
)
|
||||
weight = columns.TemplateColumn(
|
||||
verbose_name=_('Weight'),
|
||||
template_code=WEIGHT,
|
||||
order_by=('_abs_weight', 'weight_unit')
|
||||
)
|
||||
max_weight = columns.TemplateColumn(
|
||||
verbose_name=_('Max Weight'),
|
||||
template_code=WEIGHT,
|
||||
order_by=('_abs_max_weight', 'weight_unit')
|
||||
)
|
||||
comments = columns.MarkdownColumn(
|
||||
verbose_name=_('Comments'),
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:rack_list'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = RackType
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'manufacturer', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width',
|
||||
'outer_depth', 'mounting_depth', 'weight', 'max_weight', 'description', 'comments', 'tags', 'created',
|
||||
'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'manufacturer', 'type', 'u_height', 'description',
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
@@ -114,9 +170,9 @@ class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
model = Rack
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role', 'serial',
|
||||
'asset_tag', 'type', 'u_height', 'starting_unit', 'width', 'outer_width', 'outer_depth', 'mounting_depth',
|
||||
'weight', 'max_weight', 'comments', 'device_count', 'get_utilization', 'get_power_utilization',
|
||||
'description', 'contacts', 'tags', 'created', 'last_updated',
|
||||
'asset_tag', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width', 'outer_depth',
|
||||
'mounting_depth', 'weight', 'max_weight', 'comments', 'device_count', 'get_utilization',
|
||||
'get_power_utilization', 'description', 'contacts', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
|
||||
|
||||
@@ -274,6 +274,47 @@ class RackRoleTest(APIViewTestCases.APIViewTestCase):
|
||||
RackRole.objects.bulk_create(rack_roles)
|
||||
|
||||
|
||||
class RackTypeTest(APIViewTestCases.APIViewTestCase):
|
||||
model = RackType
|
||||
brief_fields = ['description', 'display', 'id', 'manufacturer', 'name', 'slug', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'new description',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
manufacturers = (
|
||||
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
|
||||
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
|
||||
)
|
||||
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 = [
|
||||
{
|
||||
'manufacturer': manufacturers[1].pk,
|
||||
'name': 'Rack Type 4',
|
||||
'slug': 'rack-type-4',
|
||||
},
|
||||
{
|
||||
'manufacturer': manufacturers[1].pk,
|
||||
'name': 'Rack Type 5',
|
||||
'slug': 'rack-type-5',
|
||||
},
|
||||
{
|
||||
'manufacturer': manufacturers[1].pk,
|
||||
'name': 'Rack Type 6',
|
||||
'slug': 'rack-type-6',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class RackTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Rack
|
||||
brief_fields = ['description', 'device_count', 'display', 'id', 'name', 'url']
|
||||
|
||||
@@ -468,6 +468,152 @@ 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):
|
||||
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',
|
||||
form_factor=RackFormFactorChoices.TYPE_2POST,
|
||||
width=RackWidthChoices.WIDTH_19IN,
|
||||
u_height=42,
|
||||
starting_unit=1,
|
||||
desc_units=False,
|
||||
outer_width=100,
|
||||
outer_depth=100,
|
||||
outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER,
|
||||
mounting_depth=100,
|
||||
weight=10,
|
||||
max_weight=1000,
|
||||
weight_unit=WeightUnitChoices.UNIT_POUND,
|
||||
description='foobar1'
|
||||
),
|
||||
RackType(
|
||||
manufacturer=manufacturers[1],
|
||||
name='RackType 2',
|
||||
slug='rack-type-2',
|
||||
form_factor=RackFormFactorChoices.TYPE_4POST,
|
||||
width=RackWidthChoices.WIDTH_21IN,
|
||||
u_height=43,
|
||||
starting_unit=2,
|
||||
desc_units=False,
|
||||
outer_width=200,
|
||||
outer_depth=200,
|
||||
outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER,
|
||||
mounting_depth=200,
|
||||
weight=20,
|
||||
max_weight=2000,
|
||||
weight_unit=WeightUnitChoices.UNIT_POUND,
|
||||
description='foobar2'
|
||||
),
|
||||
RackType(
|
||||
manufacturer=manufacturers[2],
|
||||
name='RackType 3',
|
||||
slug='rack-type-3',
|
||||
form_factor=RackFormFactorChoices.TYPE_CABINET,
|
||||
width=RackWidthChoices.WIDTH_23IN,
|
||||
u_height=44,
|
||||
starting_unit=3,
|
||||
desc_units=True,
|
||||
outer_width=300,
|
||||
outer_depth=300,
|
||||
outer_unit=RackDimensionUnitChoices.UNIT_INCH,
|
||||
mounting_depth=300,
|
||||
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_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)
|
||||
|
||||
def test_slug(self):
|
||||
params = {'slug': ['rack-type-1', 'rack-type-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_form_factor(self):
|
||||
params = {'form_factor': [RackFormFactorChoices.TYPE_2POST, RackFormFactorChoices.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, 2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
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(RackType.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_mounting_depth(self):
|
||||
params = {'mounting_depth': [100, 200]}
|
||||
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
|
||||
@@ -540,7 +686,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
role=rack_roles[0],
|
||||
serial='ABC',
|
||||
asset_tag='1001',
|
||||
type=RackTypeChoices.TYPE_2POST,
|
||||
form_factor=RackFormFactorChoices.TYPE_2POST,
|
||||
width=RackWidthChoices.WIDTH_19IN,
|
||||
u_height=42,
|
||||
desc_units=False,
|
||||
@@ -562,7 +708,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
role=rack_roles[1],
|
||||
serial='DEF',
|
||||
asset_tag='1002',
|
||||
type=RackTypeChoices.TYPE_4POST,
|
||||
form_factor=RackFormFactorChoices.TYPE_4POST,
|
||||
width=RackWidthChoices.WIDTH_21IN,
|
||||
u_height=43,
|
||||
desc_units=False,
|
||||
@@ -584,7 +730,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
role=rack_roles[2],
|
||||
serial='GHI',
|
||||
asset_tag='1003',
|
||||
type=RackTypeChoices.TYPE_CABINET,
|
||||
form_factor=RackFormFactorChoices.TYPE_CABINET,
|
||||
width=RackWidthChoices.WIDTH_23IN,
|
||||
u_height=44,
|
||||
desc_units=True,
|
||||
@@ -619,8 +765,8 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
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]}
|
||||
def test_form_factor(self):
|
||||
params = {'form_factor': [RackFormFactorChoices.TYPE_2POST, RackFormFactorChoices.TYPE_4POST]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_width(self):
|
||||
|
||||
@@ -74,6 +74,61 @@ class LocationTestCase(TestCase):
|
||||
self.assertEqual(PowerPanel.objects.get(pk=powerpanel1.pk).site, site_b)
|
||||
|
||||
|
||||
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,
|
||||
u_height=22,
|
||||
starting_unit=3,
|
||||
desc_units=True,
|
||||
outer_width=444,
|
||||
outer_depth=5,
|
||||
outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER,
|
||||
weight=66,
|
||||
weight_unit=WeightUnitChoices.UNIT_POUND,
|
||||
max_weight=7777,
|
||||
mounting_depth=8,
|
||||
)
|
||||
|
||||
def test_rack_creation(self):
|
||||
rack_type = RackType.objects.first()
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1'),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
locations = (
|
||||
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||
)
|
||||
for location in locations:
|
||||
location.save()
|
||||
|
||||
rack = Rack.objects.create(
|
||||
name='Rack 1',
|
||||
facility_id='A101',
|
||||
site=sites[0],
|
||||
location=locations[0],
|
||||
rack_type=rack_type
|
||||
)
|
||||
self.assertEqual(rack.width, rack_type.width)
|
||||
self.assertEqual(rack.u_height, rack_type.u_height)
|
||||
self.assertEqual(rack.starting_unit, rack_type.starting_unit)
|
||||
self.assertEqual(rack.desc_units, rack_type.desc_units)
|
||||
self.assertEqual(rack.outer_width, rack_type.outer_width)
|
||||
self.assertEqual(rack.outer_depth, rack_type.outer_depth)
|
||||
self.assertEqual(rack.outer_unit, rack_type.outer_unit)
|
||||
self.assertEqual(rack.weight, rack_type.weight)
|
||||
self.assertEqual(rack.weight_unit, rack_type.weight_unit)
|
||||
self.assertEqual(rack.max_weight, rack_type.max_weight)
|
||||
self.assertEqual(rack.mounting_depth, rack_type.mounting_depth)
|
||||
|
||||
|
||||
class RackTestCase(TestCase):
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -336,6 +336,75 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
|
||||
class RackTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = RackType
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
manufacturers = (
|
||||
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
|
||||
Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
|
||||
)
|
||||
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': RackFormFactorChoices.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 = (
|
||||
"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"{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': RackFormFactorChoices.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
|
||||
|
||||
@@ -380,7 +449,7 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'role': rackroles[1].pk,
|
||||
'serial': '123456',
|
||||
'asset_tag': 'ABCDEF',
|
||||
'type': RackTypeChoices.TYPE_CABINET,
|
||||
'form_factor': RackFormFactorChoices.TYPE_CABINET,
|
||||
'width': RackWidthChoices.WIDTH_19IN,
|
||||
'u_height': 48,
|
||||
'desc_units': False,
|
||||
@@ -416,7 +485,7 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'status': RackStatusChoices.STATUS_DEPRECATED,
|
||||
'role': rackroles[1].pk,
|
||||
'serial': '654321',
|
||||
'type': RackTypeChoices.TYPE_4POST,
|
||||
'form_factor': RackFormFactorChoices.TYPE_4POST,
|
||||
'width': RackWidthChoices.WIDTH_23IN,
|
||||
'u_height': 49,
|
||||
'desc_units': True,
|
||||
|
||||
@@ -63,6 +63,14 @@ urlpatterns = [
|
||||
path('racks/delete/', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'),
|
||||
path('racks/<int:pk>/', include(get_model_urls('dcim', 'rack'))),
|
||||
|
||||
# Rack Types
|
||||
path('rack-types/', views.RackTypeListView.as_view(), name='racktype_list'),
|
||||
path('rack-types/add/', views.RackTypeEditView.as_view(), name='racktype_add'),
|
||||
path('rack-types/import/', views.RackTypeBulkImportView.as_view(), name='racktype_import'),
|
||||
path('rack-types/edit/', views.RackTypeBulkEditView.as_view(), name='racktype_bulk_edit'),
|
||||
path('rack-types/delete/', views.RackTypeBulkDeleteView.as_view(), name='racktype_bulk_delete'),
|
||||
path('rack-types/<int:pk>/', include(get_model_urls('dcim', 'racktype'))),
|
||||
|
||||
# Manufacturers
|
||||
path('manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'),
|
||||
path('manufacturers/add/', views.ManufacturerEditView.as_view(), name='manufacturer_add'),
|
||||
|
||||
@@ -579,6 +579,56 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView):
|
||||
table = tables.RackRoleTable
|
||||
|
||||
|
||||
#
|
||||
# RackTypes
|
||||
#
|
||||
|
||||
class RackTypeListView(generic.ObjectListView):
|
||||
queryset = RackType.objects.all()
|
||||
filterset = filtersets.RackTypeFilterSet
|
||||
filterset_form = forms.RackTypeFilterForm
|
||||
table = tables.RackTypeTable
|
||||
|
||||
|
||||
@register_model_view(RackType)
|
||||
class RackTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = RackType.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(RackType, 'edit')
|
||||
class RackTypeEditView(generic.ObjectEditView):
|
||||
queryset = RackType.objects.all()
|
||||
form = forms.RackTypeForm
|
||||
|
||||
|
||||
@register_model_view(RackType, 'delete')
|
||||
class RackTypeDeleteView(generic.ObjectDeleteView):
|
||||
queryset = RackType.objects.all()
|
||||
|
||||
|
||||
class RackTypeBulkImportView(generic.BulkImportView):
|
||||
queryset = RackType.objects.all()
|
||||
model_form = forms.RackTypeImportForm
|
||||
|
||||
|
||||
class RackTypeBulkEditView(generic.BulkEditView):
|
||||
queryset = RackType.objects.all()
|
||||
filterset = filtersets.RackTypeFilterSet
|
||||
table = tables.RackTypeTable
|
||||
form = forms.RackTypeBulkEditForm
|
||||
|
||||
|
||||
class RackTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = RackType.objects.all()
|
||||
filterset = filtersets.RackTypeFilterSet
|
||||
table = tables.RackTypeTable
|
||||
|
||||
|
||||
#
|
||||
# Racks
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user