mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-13 16:47:34 -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:
parent
b0e7294bc1
commit
5a6ffde67e
@ -56,6 +56,10 @@ A site typically represents a building within a region and/or site group. Each s
|
||||
|
||||
A location can be any logical subdivision within a building, such as a floor or room. Like regions and site groups, locations can be nested into a self-recursive hierarchy for maximum flexibility. And like sites, each location has an operational status assigned to it.
|
||||
|
||||
## Rack Types
|
||||
|
||||
A rack type represents a unique specification of a rack which exists in the real world. Each rack type can be setup with weight, height, and unit ordering. New racks of this type can then be created in NetBox, and any associated specifications will be automatically replicated from the device type.
|
||||
|
||||
## Racks
|
||||
|
||||
Finally, NetBox models each equipment rack as a discrete object within a site and location. These are physical objects into which devices are installed. Each rack can be assigned an operational status, type, facility ID, and other attributes related to inventory tracking. Each rack also must define a height (in rack units) and width, and may optionally specify its physical dimensions.
|
||||
|
@ -20,6 +20,10 @@ The [location](./location.md) within a site where the rack has been installed (o
|
||||
|
||||
The rack's name or identifier. Must be unique to the rack's location, if assigned.
|
||||
|
||||
### Rack Type
|
||||
|
||||
The [physical type](./racktype.md) of this rack. The rack type defines physical attributes such as height and weight.
|
||||
|
||||
### Status
|
||||
|
||||
Operational status.
|
||||
@ -43,44 +47,5 @@ The unique physical serial number assigned to this rack.
|
||||
|
||||
A unique, locally-administered label used to identify hardware resources.
|
||||
|
||||
### Type
|
||||
|
||||
A rack can be designated as one of the following types:
|
||||
|
||||
* 2-post frame
|
||||
* 4-post frame
|
||||
* 4-post cabinet
|
||||
* Wall-mounted frame
|
||||
* Wall-mounted cabinet
|
||||
|
||||
### Width
|
||||
|
||||
The canonical distance between the two vertical rails on a face. (This is typically 19 inches, however other standard widths exist.)
|
||||
|
||||
### Height
|
||||
|
||||
The height of the rack, measured in units.
|
||||
|
||||
### Starting Unit
|
||||
|
||||
The number of the numerically lowest unit in the rack. This value defaults to one, but may be higher in certain situations. For example, you may want to model only a select range of units within a shared physical rack (e.g. U13 through U24).
|
||||
|
||||
### Outer Dimensions
|
||||
|
||||
The external width and depth of the rack can be tracked to aid in floorplan calculations. These measurements must be designated in either millimeters or inches.
|
||||
|
||||
### Mounting Depth
|
||||
|
||||
The maximum depth of a mounted device that the rack can accommodate, in millimeters. For four-post frames or cabinets, this is the horizontal distance between the front and rear vertical rails. (Note that this measurement does _not_ include space between the rails and the cabinet doors.)
|
||||
|
||||
### Weight
|
||||
|
||||
The numeric weight of the rack, including a unit designation (e.g. 10 kilograms or 20 pounds).
|
||||
|
||||
### Maximum Weight
|
||||
|
||||
The maximum total weight capacity for all installed devices, inclusive of the rack itself.
|
||||
|
||||
### Descending Units
|
||||
|
||||
If selected, the rack's elevation will display unit 1 at the top of the rack. (Most racks use ascending numbering, with unit 1 assigned to the bottommost position.)
|
||||
!!! note
|
||||
Some additional fields pertaining to physical attributes such as height and weight can also be defined on each rack, but should generally be defined instead on the [rack type](./racktype.md).
|
||||
|
57
docs/models/dcim/racktype.md
Normal file
57
docs/models/dcim/racktype.md
Normal file
@ -0,0 +1,57 @@
|
||||
# Rack Types
|
||||
|
||||
A rack type defines the physical characteristics of a particular model of [rack](./rack.md).
|
||||
|
||||
## Fields
|
||||
|
||||
### Manufacturer
|
||||
|
||||
The [manufacturer](./manufacturer.md) which produces this type of rack.
|
||||
|
||||
### Name
|
||||
|
||||
The unique name of the rack type.
|
||||
|
||||
### Form Factor
|
||||
|
||||
A rack can be designated as one of the following form factors:
|
||||
|
||||
* 2-post frame
|
||||
* 4-post frame
|
||||
* 4-post cabinet
|
||||
* Wall-mounted frame
|
||||
* Wall-mounted cabinet
|
||||
|
||||
### Width
|
||||
|
||||
The canonical distance between the two vertical rails on a face. (This is typically 19 inches, however other standard widths exist.)
|
||||
|
||||
### Height
|
||||
|
||||
The height of the rack, measured in units.
|
||||
|
||||
### Starting Unit
|
||||
|
||||
The number of the numerically lowest unit in the rack. This value defaults to one, but may be higher in certain situations. For example, you may want to model only a select range of units within a shared physical rack (e.g. U13 through U24).
|
||||
|
||||
### Outer Dimensions
|
||||
|
||||
The external width and depth of the rack can be tracked to aid in floorplan calculations. These measurements must be designated in either millimeters or inches.
|
||||
|
||||
### Mounting Depth
|
||||
|
||||
The maximum depth of a mounted device that the rack can accommodate, in millimeters. For four-post frames or cabinets, this is the horizontal distance between the front and rear vertical rails. (Note that this measurement does _not_ include space between the rails and the cabinet doors.)
|
||||
|
||||
### Weight
|
||||
|
||||
The numeric weight of the rack, including a unit designation (e.g. 10 kilograms or 20 pounds).
|
||||
|
||||
### Maximum Weight
|
||||
|
||||
The maximum total weight capacity for all installed devices, inclusive of the rack itself.
|
||||
|
||||
### Descending Units
|
||||
|
||||
If selected, the rack's elevation will display unit 1 at the top of the rack. (Most racks use ascending numbering, with unit 1 assigned to the bottommost position.)
|
||||
|
||||
|
@ -206,6 +206,7 @@ nav:
|
||||
- Rack: 'models/dcim/rack.md'
|
||||
- RackReservation: 'models/dcim/rackreservation.md'
|
||||
- RackRole: 'models/dcim/rackrole.md'
|
||||
- RackType: 'models/dcim/racktype.md'
|
||||
- RearPort: 'models/dcim/rearport.md'
|
||||
- RearPortTemplate: 'models/dcim/rearporttemplate.md'
|
||||
- Region: 'models/dcim/region.md'
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -1184,6 +1184,7 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
'rack',
|
||||
'rackreservation',
|
||||
'rackrole',
|
||||
'racktype',
|
||||
'rearport',
|
||||
'region',
|
||||
'rir',
|
||||
|
@ -24,6 +24,7 @@ ORGANIZATION_MENU = Menu(
|
||||
label=_('Racks'),
|
||||
items=(
|
||||
get_model_item('dcim', 'rack', _('Racks')),
|
||||
get_model_item('dcim', 'racktype', _('Rack Types')),
|
||||
get_model_item('dcim', 'rackrole', _('Rack Roles')),
|
||||
get_model_item('dcim', 'rackreservation', _('Reservations')),
|
||||
MenuItem(
|
||||
|
48
netbox/templates/dcim/inc/panels/racktype_dimensions.html
Normal file
48
netbox/templates/dcim/inc/panels/racktype_dimensions.html
Normal file
@ -0,0 +1,48 @@
|
||||
{% load i18n %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">{% trans "Dimensions" %}</h5>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Form factor" %}</th>
|
||||
<td>{{ object.get_form_factor_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Width" %}</th>
|
||||
<td>{{ object.get_width_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Height" %}</th>
|
||||
<td>{{ object.u_height }}U</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Outer Width" %}</th>
|
||||
<td>
|
||||
{% if object.outer_width %}
|
||||
{{ object.outer_width }} {{ object.get_outer_unit_display }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Outer Depth" %}</th>
|
||||
<td>
|
||||
{% if object.outer_depth %}
|
||||
{{ object.outer_depth }} {{ object.get_outer_unit_display }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Mounting Depth" %}</th>
|
||||
<td>
|
||||
{% if object.mounting_depth %}
|
||||
{{ object.mounting_depth }} {% trans "Millimeters" %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
14
netbox/templates/dcim/inc/panels/racktype_numbering.html
Normal file
14
netbox/templates/dcim/inc/panels/racktype_numbering.html
Normal file
@ -0,0 +1,14 @@
|
||||
{% load i18n %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">{% trans "Numbering" %}</h5>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Starting Unit" %}</th>
|
||||
<td>{{ object.starting_unit }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Descending Units" %}</th>
|
||||
<td>{% checkmark object.desc_units %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
@ -9,157 +9,107 @@
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-xl-5">
|
||||
<div class="card">
|
||||
<h5 class="card-header">{% trans "Rack" %}</h5>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Region" %}</th>
|
||||
<td>
|
||||
{% nested_tree object.site.region %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Site" %}</th>
|
||||
<td>{{ object.site|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Location" %}</th>
|
||||
<td>{% nested_tree object.location %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Facility ID" %}</th>
|
||||
<td>{{ object.facility_id|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Status" %}</th>
|
||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Role" %}</th>
|
||||
<td>{{ object.role|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Serial Number" %}</th>
|
||||
<td class="font-monospace">{{ object.serial|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Asset Tag" %}</th>
|
||||
<td class="font-monospace">{{ object.asset_tag|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Space Utilization" %}</th>
|
||||
<td>{% utilization_graph object.get_utilization %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Power Utilization" %}</th>
|
||||
<td>{% utilization_graph object.get_power_utilization %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h5 class="card-header">{% trans "Dimensions" %}</h5>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Type" %}</th>
|
||||
<td>
|
||||
{% if object.type %}
|
||||
{{ object.get_type_display }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Width" %}</th>
|
||||
<td>{{ object.get_width_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Height" %}</th>
|
||||
<td>{{ object.u_height }}U ({% if object.desc_units %}{% trans "descending" %}{% else %}{% trans "ascending" %}{% endif %})</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Starting Unit" %}</th>
|
||||
<td>
|
||||
{{ object.starting_unit }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Outer Width" %}</th>
|
||||
<td>
|
||||
{% if object.outer_width %}
|
||||
{{ object.outer_width }} {{ object.get_outer_unit_display }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Outer Depth" %}</th>
|
||||
<td>
|
||||
{% if object.outer_depth %}
|
||||
{{ object.outer_depth }} {{ object.get_outer_unit_display }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Mounting Depth" %}</th>
|
||||
<td>
|
||||
{% if object.mounting_depth %}
|
||||
{{ object.mounting_depth }} {% trans "Millimeters" %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Rack Weight" %}</th>
|
||||
<td>
|
||||
{% if object.weight %}
|
||||
{{ object.weight|floatformat }} {{ object.get_weight_unit_display }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Maximum Weight" %}</th>
|
||||
<td>
|
||||
{% if object.max_weight %}
|
||||
{{ object.max_weight }} {{ object.get_weight_unit_display }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Total Weight" %}</th>
|
||||
<td>
|
||||
{{ object.total_weight|floatformat }} {% trans "Kilograms" %}
|
||||
({{ object.total_weight|kg_to_pounds|floatformat }} {% trans "Pounds" %})
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/image_attachments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">{% trans "Rack" %}</h5>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Region" %}</th>
|
||||
<td>{% nested_tree object.site.region %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Site" %}</th>
|
||||
<td>{{ object.site|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Location" %}</th>
|
||||
<td>{% nested_tree object.location %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Facility ID" %}</th>
|
||||
<td>{{ object.facility_id|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Status" %}</th>
|
||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Rack Type" %}</th>
|
||||
<td>{{ object.rack_type|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Role" %}</th>
|
||||
<td>{{ object.role|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Serial Number" %}</th>
|
||||
<td class="font-monospace">{{ object.serial|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Asset Tag" %}</th>
|
||||
<td class="font-monospace">{{ object.asset_tag|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Space Utilization" %}</th>
|
||||
<td>{% utilization_graph object.get_utilization %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Power Utilization" %}</th>
|
||||
<td>{% utilization_graph object.get_power_utilization %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'dcim/inc/panels/racktype_dimensions.html' %}
|
||||
{% include 'dcim/inc/panels/racktype_numbering.html' %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">{% trans "Weight" %}</h5>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Rack Weight" %}</th>
|
||||
<td>
|
||||
{% if object.weight %}
|
||||
{{ object.weight|floatformat }} {{ object.get_weight_unit_display }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Maximum Weight" %}</th>
|
||||
<td>
|
||||
{% if object.max_weight %}
|
||||
{{ object.max_weight }} {{ object.get_weight_unit_display }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Total Weight" %}</th>
|
||||
<td>
|
||||
{{ object.total_weight|floatformat }} {% trans "Kilograms" %}
|
||||
({{ object.total_weight|kg_to_pounds|floatformat }} {% trans "Pounds" %})
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/image_attachments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-xl-7">
|
||||
<div class="text-end mb-4">
|
||||
@ -170,26 +120,26 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="row" style="margin-bottom: 20px">
|
||||
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<div style="margin-left: 30px">
|
||||
<h4>{% trans "Front" %}</h4>
|
||||
{% include 'dcim/inc/rack_elevation.html' with face='front' extra_params=svg_extra %}
|
||||
</div>
|
||||
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<div style="margin-left: 30px">
|
||||
<h4>{% trans "Front" %}</h4>
|
||||
{% include 'dcim/inc/rack_elevation.html' with face='front' extra_params=svg_extra %}
|
||||
</div>
|
||||
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<div style="margin-left: 30px">
|
||||
<h4>{% trans "Rear" %}</h4>
|
||||
{% include 'dcim/inc/rack_elevation.html' with face='rear' extra_params=svg_extra %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-6 col-sm-6 col-xs-12 text-center">
|
||||
<div style="margin-left: 30px">
|
||||
<h4>{% trans "Rear" %}</h4>
|
||||
{% include 'dcim/inc/rack_elevation.html' with face='rear' extra_params=svg_extra %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
71
netbox/templates/dcim/racktype.html
Normal file
71
netbox/templates/dcim/racktype.html
Normal file
@ -0,0 +1,71 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load static %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
{% load mptt %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-6">
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'dcim/inc/panels/racktype_dimensions.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-6">
|
||||
{% include 'dcim/inc/panels/racktype_numbering.html' %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">{% trans "Weight" %}</h5>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Rack Weight" %}</th>
|
||||
<td>
|
||||
{% if object.weight %}
|
||||
{{ object.weight|floatformat }} {{ object.get_weight_unit_display }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Maximum Weight" %}</th>
|
||||
<td>
|
||||
{% if object.max_weight %}
|
||||
{{ object.max_weight }} {{ object.get_weight_unit_display }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
Reference in New Issue
Block a user