mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 12:12:53 -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.
|
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
|
## 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.
|
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.
|
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
|
### Status
|
||||||
|
|
||||||
Operational 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.
|
A unique, locally-administered label used to identify hardware resources.
|
||||||
|
|
||||||
### Type
|
!!! 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).
|
||||||
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.)
|
|
||||||
|
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'
|
- Rack: 'models/dcim/rack.md'
|
||||||
- RackReservation: 'models/dcim/rackreservation.md'
|
- RackReservation: 'models/dcim/rackreservation.md'
|
||||||
- RackRole: 'models/dcim/rackrole.md'
|
- RackRole: 'models/dcim/rackrole.md'
|
||||||
|
- RackType: 'models/dcim/racktype.md'
|
||||||
- RearPort: 'models/dcim/rearport.md'
|
- RearPort: 'models/dcim/rearport.md'
|
||||||
- RearPortTemplate: 'models/dcim/rearporttemplate.md'
|
- RearPortTemplate: 'models/dcim/rearporttemplate.md'
|
||||||
- Region: 'models/dcim/region.md'
|
- Region: 'models/dcim/region.md'
|
||||||
|
@ -3,12 +3,13 @@ from rest_framework import serializers
|
|||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants 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.fields import ChoiceField, RelatedObjectCountField
|
||||||
from netbox.api.serializers import NetBoxModelSerializer
|
from netbox.api.serializers import NetBoxModelSerializer
|
||||||
from netbox.config import ConfigItem
|
from netbox.config import ConfigItem
|
||||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||||
from users.api.serializers_.users import UserSerializer
|
from users.api.serializers_.users import UserSerializer
|
||||||
|
from .manufacturers import ManufacturerSerializer
|
||||||
from .sites import LocationSerializer, SiteSerializer
|
from .sites import LocationSerializer, SiteSerializer
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -16,6 +17,7 @@ __all__ = (
|
|||||||
'RackReservationSerializer',
|
'RackReservationSerializer',
|
||||||
'RackRoleSerializer',
|
'RackRoleSerializer',
|
||||||
'RackSerializer',
|
'RackSerializer',
|
||||||
|
'RackTypeSerializer',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -33,18 +35,84 @@ class RackRoleSerializer(NetBoxModelSerializer):
|
|||||||
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count')
|
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count')
|
||||||
|
|
||||||
|
|
||||||
class RackSerializer(NetBoxModelSerializer):
|
class RackBaseSerializer(NetBoxModelSerializer):
|
||||||
site = SiteSerializer(nested=True)
|
form_factor = ChoiceField(
|
||||||
location = LocationSerializer(nested=True, required=False, allow_null=True, default=None)
|
choices=RackFormFactorChoices,
|
||||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
allow_blank=True,
|
||||||
status = ChoiceField(choices=RackStatusChoices, required=False)
|
required=False,
|
||||||
role = RackRoleSerializer(nested=True, required=False, allow_null=True)
|
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'),
|
width = ChoiceField(
|
||||||
default=None)
|
choices=RackWidthChoices,
|
||||||
width = ChoiceField(choices=RackWidthChoices, required=False)
|
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)
|
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
|
# Related object counts
|
||||||
device_count = RelatedObjectCountField('devices')
|
device_count = RelatedObjectCountField('devices')
|
||||||
@ -54,9 +122,10 @@ class RackSerializer(NetBoxModelSerializer):
|
|||||||
model = Rack
|
model = Rack
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status',
|
'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',
|
'role', 'serial', 'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'weight',
|
||||||
'weight_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'description',
|
'max_weight', 'weight_unit', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth',
|
||||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count',
|
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
|
||||||
|
'powerfeed_count',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count')
|
brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count')
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ router.register('sites', views.SiteViewSet)
|
|||||||
|
|
||||||
# Racks
|
# Racks
|
||||||
router.register('locations', views.LocationViewSet)
|
router.register('locations', views.LocationViewSet)
|
||||||
|
router.register('rack-types', views.RackTypeViewSet)
|
||||||
router.register('rack-roles', views.RackRoleViewSet)
|
router.register('rack-roles', views.RackRoleViewSet)
|
||||||
router.register('racks', views.RackViewSet)
|
router.register('racks', views.RackViewSet)
|
||||||
router.register('rack-reservations', views.RackReservationViewSet)
|
router.register('rack-reservations', views.RackReservationViewSet)
|
||||||
|
@ -161,6 +161,16 @@ class RackRoleViewSet(NetBoxModelViewSet):
|
|||||||
filterset_class = filtersets.RackRoleFilterSet
|
filterset_class = filtersets.RackRoleFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Rack Types
|
||||||
|
#
|
||||||
|
|
||||||
|
class RackTypeViewSet(NetBoxModelViewSet):
|
||||||
|
queryset = RackType.objects.all()
|
||||||
|
serializer_class = serializers.RackTypeSerializer
|
||||||
|
filterset_class = filtersets.RackTypeFilterSet
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Racks
|
# Racks
|
||||||
#
|
#
|
||||||
|
@ -51,7 +51,7 @@ class LocationStatusChoices(ChoiceSet):
|
|||||||
# Racks
|
# Racks
|
||||||
#
|
#
|
||||||
|
|
||||||
class RackTypeChoices(ChoiceSet):
|
class RackFormFactorChoices(ChoiceSet):
|
||||||
|
|
||||||
TYPE_2POST = '2-post-frame'
|
TYPE_2POST = '2-post-frame'
|
||||||
TYPE_4POST = '4-post-frame'
|
TYPE_4POST = '4-post-frame'
|
||||||
|
@ -69,6 +69,7 @@ __all__ = (
|
|||||||
'RackFilterSet',
|
'RackFilterSet',
|
||||||
'RackReservationFilterSet',
|
'RackReservationFilterSet',
|
||||||
'RackRoleFilterSet',
|
'RackRoleFilterSet',
|
||||||
|
'RackTypeFilterSet',
|
||||||
'RearPortFilterSet',
|
'RearPortFilterSet',
|
||||||
'RearPortTemplateFilterSet',
|
'RearPortTemplateFilterSet',
|
||||||
'RegionFilterSet',
|
'RegionFilterSet',
|
||||||
@ -289,6 +290,41 @@ class RackRoleFilterSet(OrganizationalModelFilterSet):
|
|||||||
fields = ('id', 'name', 'slug', 'color', 'description')
|
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):
|
class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||||
region_id = TreeNodeMultipleChoiceFilter(
|
region_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
@ -339,12 +375,22 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label=_('Location (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(
|
status = django_filters.MultipleChoiceFilter(
|
||||||
choices=RackStatusChoices,
|
choices=RackStatusChoices,
|
||||||
null_value=None
|
null_value=None
|
||||||
)
|
)
|
||||||
type = django_filters.MultipleChoiceFilter(
|
form_factor = django_filters.MultipleChoiceFilter(
|
||||||
choices=RackTypeChoices
|
choices=RackFormFactorChoices
|
||||||
)
|
)
|
||||||
width = django_filters.MultipleChoiceFilter(
|
width = django_filters.MultipleChoiceFilter(
|
||||||
choices=RackWidthChoices
|
choices=RackWidthChoices
|
||||||
|
@ -13,7 +13,7 @@ from netbox.forms import NetBoxModelBulkEditForm
|
|||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
|
from utilities.forms import BulkEditForm, add_blank_choice, form_from_model
|
||||||
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
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 utilities.forms.widgets import BulkEditNullBooleanSelect, NumberWithOptions
|
||||||
from wireless.models import WirelessLAN, WirelessLANGroup
|
from wireless.models import WirelessLAN, WirelessLANGroup
|
||||||
from wireless.choices import WirelessRoleChoices
|
from wireless.choices import WirelessRoleChoices
|
||||||
@ -52,6 +52,7 @@ __all__ = (
|
|||||||
'RackBulkEditForm',
|
'RackBulkEditForm',
|
||||||
'RackReservationBulkEditForm',
|
'RackReservationBulkEditForm',
|
||||||
'RackRoleBulkEditForm',
|
'RackRoleBulkEditForm',
|
||||||
|
'RackTypeBulkEditForm',
|
||||||
'RearPortBulkEditForm',
|
'RearPortBulkEditForm',
|
||||||
'RearPortTemplateBulkEditForm',
|
'RearPortTemplateBulkEditForm',
|
||||||
'RegionBulkEditForm',
|
'RegionBulkEditForm',
|
||||||
@ -218,6 +219,97 @@ class RackRoleBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
nullable_fields = ('color', 'description')
|
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):
|
class RackBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
region = DynamicModelChoiceField(
|
region = DynamicModelChoiceField(
|
||||||
label=_('Region'),
|
label=_('Region'),
|
||||||
@ -278,9 +370,9 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
max_length=50,
|
max_length=50,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
type = forms.ChoiceField(
|
form_factor = forms.ChoiceField(
|
||||||
label=_('Type'),
|
label=_('Form factor'),
|
||||||
choices=add_blank_choice(RackTypeChoices),
|
choices=add_blank_choice(RackFormFactorChoices),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
width = forms.ChoiceField(
|
width = forms.ChoiceField(
|
||||||
@ -345,8 +437,8 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
FieldSet('status', 'role', 'tenant', 'serial', 'asset_tag', 'description', name=_('Rack')),
|
FieldSet('status', 'role', 'tenant', 'serial', 'asset_tag', 'description', name=_('Rack')),
|
||||||
FieldSet('region', 'site_group', 'site', 'location', name=_('Location')),
|
FieldSet('region', 'site_group', 'site', 'location', name=_('Location')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth',
|
'form_factor', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
|
||||||
name=_('Hardware')
|
'mounting_depth', name=_('Hardware')
|
||||||
),
|
),
|
||||||
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
FieldSet('weight', 'max_weight', 'weight_unit', name=_('Weight')),
|
||||||
)
|
)
|
||||||
|
@ -45,6 +45,7 @@ __all__ = (
|
|||||||
'RackImportForm',
|
'RackImportForm',
|
||||||
'RackReservationImportForm',
|
'RackReservationImportForm',
|
||||||
'RackRoleImportForm',
|
'RackRoleImportForm',
|
||||||
|
'RackTypeImportForm',
|
||||||
'RearPortImportForm',
|
'RearPortImportForm',
|
||||||
'RegionImportForm',
|
'RegionImportForm',
|
||||||
'SiteImportForm',
|
'SiteImportForm',
|
||||||
@ -176,6 +177,54 @@ class RackRoleImportForm(NetBoxModelImportForm):
|
|||||||
fields = ('name', 'slug', 'color', 'description', 'tags')
|
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):
|
class RackImportForm(NetBoxModelImportForm):
|
||||||
site = CSVModelChoiceField(
|
site = CSVModelChoiceField(
|
||||||
label=_('Site'),
|
label=_('Site'),
|
||||||
@ -207,11 +256,11 @@ class RackImportForm(NetBoxModelImportForm):
|
|||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
help_text=_('Name of assigned role')
|
help_text=_('Name of assigned role')
|
||||||
)
|
)
|
||||||
type = CSVChoiceField(
|
form_factor = CSVChoiceField(
|
||||||
label=_('Type'),
|
label=_('Type'),
|
||||||
choices=RackTypeChoices,
|
choices=RackFormFactorChoices,
|
||||||
required=False,
|
required=False,
|
||||||
help_text=_('Rack type')
|
help_text=_('Form factor')
|
||||||
)
|
)
|
||||||
width = forms.ChoiceField(
|
width = forms.ChoiceField(
|
||||||
label=_('Width'),
|
label=_('Width'),
|
||||||
@ -234,7 +283,7 @@ class RackImportForm(NetBoxModelImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = (
|
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',
|
'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight',
|
||||||
'max_weight', 'weight_unit', 'description', 'comments', 'tags',
|
'max_weight', 'weight_unit', 'description', 'comments', 'tags',
|
||||||
)
|
)
|
||||||
|
@ -48,6 +48,7 @@ __all__ = (
|
|||||||
'RackElevationFilterForm',
|
'RackElevationFilterForm',
|
||||||
'RackReservationFilterForm',
|
'RackReservationFilterForm',
|
||||||
'RackRoleFilterForm',
|
'RackRoleFilterForm',
|
||||||
|
'RackTypeFilterForm',
|
||||||
'RearPortFilterForm',
|
'RearPortFilterForm',
|
||||||
'RegionFilterForm',
|
'RegionFilterForm',
|
||||||
'SiteFilterForm',
|
'SiteFilterForm',
|
||||||
@ -240,16 +241,77 @@ class RackRoleFilterForm(NetBoxModelFilterSetForm):
|
|||||||
tag = TagFilterField(model)
|
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
|
model = Rack
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Location')),
|
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('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('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')
|
selector_fields = ('filter_id', 'q', 'region_id', 'site_group_id', 'site_id', 'location_id')
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
@ -284,16 +346,6 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
|
|||||||
choices=RackStatusChoices,
|
choices=RackStatusChoices,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
type = forms.MultipleChoiceField(
|
|
||||||
label=_('Type'),
|
|
||||||
choices=RackTypeChoices,
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
width = forms.MultipleChoiceField(
|
|
||||||
label=_('Width'),
|
|
||||||
choices=RackWidthChoices,
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
role_id = DynamicModelMultipleChoiceField(
|
role_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=RackRole.objects.all(),
|
queryset=RackRole.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -309,21 +361,6 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
|
|||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
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):
|
class RackElevationFilterForm(RackFilterForm):
|
||||||
|
@ -11,7 +11,7 @@ from extras.models import ConfigTemplate
|
|||||||
from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF
|
from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF
|
||||||
from netbox.forms import NetBoxModelForm
|
from netbox.forms import NetBoxModelForm
|
||||||
from tenancy.forms import TenancyForm
|
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 (
|
from utilities.forms.fields import (
|
||||||
CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField,
|
CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField,
|
||||||
)
|
)
|
||||||
@ -57,6 +57,7 @@ __all__ = (
|
|||||||
'RackForm',
|
'RackForm',
|
||||||
'RackReservationForm',
|
'RackReservationForm',
|
||||||
'RackRoleForm',
|
'RackRoleForm',
|
||||||
|
'RackTypeForm',
|
||||||
'RearPortForm',
|
'RearPortForm',
|
||||||
'RearPortTemplateForm',
|
'RearPortTemplateForm',
|
||||||
'RegionForm',
|
'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):
|
class RackForm(TenancyForm, NetBoxModelForm):
|
||||||
site = DynamicModelChoiceField(
|
site = DynamicModelChoiceField(
|
||||||
label=_('Site'),
|
label=_('Site'),
|
||||||
@ -220,28 +249,54 @@ class RackForm(TenancyForm, NetBoxModelForm):
|
|||||||
queryset=RackRole.objects.all(),
|
queryset=RackRole.objects.all(),
|
||||||
required=False
|
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()
|
comments = CommentField()
|
||||||
|
|
||||||
fieldsets = (
|
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('facility_id', 'serial', 'asset_tag', name=_('Inventory Control')),
|
||||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
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:
|
class Meta:
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = [
|
fields = [
|
||||||
'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
|
'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',
|
'asset_tag', 'rack_type', 'form_factor', 'width', 'u_height', 'starting_unit', 'desc_units', 'outer_width',
|
||||||
'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
|
'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):
|
class RackReservationForm(TenancyForm, NetBoxModelForm):
|
||||||
rack = DynamicModelChoiceField(
|
rack = DynamicModelChoiceField(
|
||||||
|
@ -38,6 +38,7 @@ __all__ = (
|
|||||||
'RackFilter',
|
'RackFilter',
|
||||||
'RackReservationFilter',
|
'RackReservationFilter',
|
||||||
'RackRoleFilter',
|
'RackRoleFilter',
|
||||||
|
'RackTypeFilter',
|
||||||
'RearPortFilter',
|
'RearPortFilter',
|
||||||
'RearPortTemplateFilter',
|
'RearPortTemplateFilter',
|
||||||
'RegionFilter',
|
'RegionFilter',
|
||||||
@ -234,6 +235,12 @@ class PowerPortTemplateFilter(BaseFilterMixin):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_django.filter(models.RackType, lookups=True)
|
||||||
|
@autotype_decorator(filtersets.RackTypeFilterSet)
|
||||||
|
class RackTypeFilter(BaseFilterMixin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.filter(models.Rack, lookups=True)
|
@strawberry_django.filter(models.Rack, lookups=True)
|
||||||
@autotype_decorator(filtersets.RackFilterSet)
|
@autotype_decorator(filtersets.RackFilterSet)
|
||||||
class RackFilter(BaseFilterMixin):
|
class RackFilter(BaseFilterMixin):
|
||||||
|
@ -159,6 +159,11 @@ class DCIMQuery:
|
|||||||
return models.PowerPortTemplate.objects.get(id=id)
|
return models.PowerPortTemplate.objects.get(id=id)
|
||||||
power_port_template_list: List[PowerPortTemplateType] = strawberry_django.field()
|
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
|
@strawberry.field
|
||||||
def rack(self, id: int) -> RackType:
|
def rack(self, id: int) -> RackType:
|
||||||
return models.Rack.objects.get(id=id)
|
return models.Rack.objects.get(id=id)
|
||||||
|
@ -50,6 +50,7 @@ __all__ = (
|
|||||||
'RackType',
|
'RackType',
|
||||||
'RackReservationType',
|
'RackReservationType',
|
||||||
'RackRoleType',
|
'RackRoleType',
|
||||||
|
'RackTypeType',
|
||||||
'RearPortType',
|
'RearPortType',
|
||||||
'RearPortTemplateType',
|
'RearPortTemplateType',
|
||||||
'RegionType',
|
'RegionType',
|
||||||
@ -606,6 +607,16 @@ class PowerPortTemplateType(ModularComponentTemplateType):
|
|||||||
poweroutlet_templates: List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
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(
|
@strawberry_django.type(
|
||||||
models.Rack,
|
models.Rack,
|
||||||
fields='__all__',
|
fields='__all__',
|
||||||
@ -618,6 +629,7 @@ class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje
|
|||||||
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
|
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
|
||||||
role: Annotated["RackRoleType", strawberry.lazy('dcim.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')]]
|
reservations: List[Annotated["RackReservationType", strawberry.lazy('dcim.graphql.types')]]
|
||||||
devices: List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]
|
devices: List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]
|
||||||
powerfeeds: List[Annotated["PowerFeedType", 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',
|
'Rack',
|
||||||
'RackReservation',
|
'RackReservation',
|
||||||
'RackRole',
|
'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
|
# Racks
|
||||||
#
|
#
|
||||||
@ -54,11 +226,24 @@ class RackRole(OrganizationalModel):
|
|||||||
return reverse('dcim:rackrole', args=[self.pk])
|
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.
|
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.
|
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(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=100
|
max_length=100
|
||||||
@ -121,73 +306,6 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
|||||||
verbose_name=_('asset tag'),
|
verbose_name=_('asset tag'),
|
||||||
help_text=_('A unique tag used to identify this rack')
|
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
|
# Generic relations
|
||||||
vlan_groups = GenericRelation(
|
vlan_groups = GenericRelation(
|
||||||
@ -198,7 +316,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
clone_fields = (
|
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',
|
'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit',
|
||||||
)
|
)
|
||||||
prerequisite_models = (
|
prerequisite_models = (
|
||||||
@ -271,6 +389,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
|||||||
})
|
})
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
self.copy_racktype_attrs()
|
||||||
|
|
||||||
# Store the given max weight (if any) in grams for use in database ordering
|
# Store the given max weight (if any) in grams for use in database ordering
|
||||||
if self.max_weight and self.weight_unit:
|
if self.max_weight and self.weight_unit:
|
||||||
@ -284,6 +403,14 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, WeightMixin):
|
|||||||
|
|
||||||
super().save(*args, **kwargs)
|
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
|
@property
|
||||||
def units(self):
|
def units(self):
|
||||||
"""
|
"""
|
||||||
|
@ -242,6 +242,17 @@ class PowerPortIndex(SearchIndex):
|
|||||||
display_attrs = ('device', 'label', 'type', 'description')
|
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
|
@register_search
|
||||||
class RackIndex(SearchIndex):
|
class RackIndex(SearchIndex):
|
||||||
model = models.Rack
|
model = models.Rack
|
||||||
|
@ -2,7 +2,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2.utils import Accessor
|
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 netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||||
from .template_code import WEIGHT
|
from .template_code import WEIGHT
|
||||||
@ -11,6 +11,7 @@ __all__ = (
|
|||||||
'RackTable',
|
'RackTable',
|
||||||
'RackReservationTable',
|
'RackReservationTable',
|
||||||
'RackRoleTable',
|
'RackRoleTable',
|
||||||
|
'RackTypeTable',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -44,6 +45,61 @@ class RackRoleTable(NetBoxTable):
|
|||||||
default_columns = ('pk', 'name', 'rack_count', 'color', 'description')
|
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
|
# Racks
|
||||||
#
|
#
|
||||||
@ -114,9 +170,9 @@ class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
|||||||
model = Rack
|
model = Rack
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role', 'serial',
|
'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',
|
'asset_tag', 'form_factor', 'u_height', 'starting_unit', 'width', 'outer_width', 'outer_depth',
|
||||||
'weight', 'max_weight', 'comments', 'device_count', 'get_utilization', 'get_power_utilization',
|
'mounting_depth', 'weight', 'max_weight', 'comments', 'device_count', 'get_utilization',
|
||||||
'description', 'contacts', 'tags', 'created', 'last_updated',
|
'get_power_utilization', 'description', 'contacts', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count',
|
'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)
|
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):
|
class RackTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = Rack
|
model = Rack
|
||||||
brief_fields = ['description', 'device_count', 'display', 'id', 'name', 'url']
|
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)
|
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):
|
class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = Rack.objects.all()
|
queryset = Rack.objects.all()
|
||||||
filterset = RackFilterSet
|
filterset = RackFilterSet
|
||||||
@ -540,7 +686,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
role=rack_roles[0],
|
role=rack_roles[0],
|
||||||
serial='ABC',
|
serial='ABC',
|
||||||
asset_tag='1001',
|
asset_tag='1001',
|
||||||
type=RackTypeChoices.TYPE_2POST,
|
form_factor=RackFormFactorChoices.TYPE_2POST,
|
||||||
width=RackWidthChoices.WIDTH_19IN,
|
width=RackWidthChoices.WIDTH_19IN,
|
||||||
u_height=42,
|
u_height=42,
|
||||||
desc_units=False,
|
desc_units=False,
|
||||||
@ -562,7 +708,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
role=rack_roles[1],
|
role=rack_roles[1],
|
||||||
serial='DEF',
|
serial='DEF',
|
||||||
asset_tag='1002',
|
asset_tag='1002',
|
||||||
type=RackTypeChoices.TYPE_4POST,
|
form_factor=RackFormFactorChoices.TYPE_4POST,
|
||||||
width=RackWidthChoices.WIDTH_21IN,
|
width=RackWidthChoices.WIDTH_21IN,
|
||||||
u_height=43,
|
u_height=43,
|
||||||
desc_units=False,
|
desc_units=False,
|
||||||
@ -584,7 +730,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
role=rack_roles[2],
|
role=rack_roles[2],
|
||||||
serial='GHI',
|
serial='GHI',
|
||||||
asset_tag='1003',
|
asset_tag='1003',
|
||||||
type=RackTypeChoices.TYPE_CABINET,
|
form_factor=RackFormFactorChoices.TYPE_CABINET,
|
||||||
width=RackWidthChoices.WIDTH_23IN,
|
width=RackWidthChoices.WIDTH_23IN,
|
||||||
u_height=44,
|
u_height=44,
|
||||||
desc_units=True,
|
desc_units=True,
|
||||||
@ -619,8 +765,8 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'description': ['foobar1', 'foobar2']}
|
params = {'description': ['foobar1', 'foobar2']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_type(self):
|
def test_form_factor(self):
|
||||||
params = {'type': [RackTypeChoices.TYPE_2POST, RackTypeChoices.TYPE_4POST]}
|
params = {'form_factor': [RackFormFactorChoices.TYPE_2POST, RackFormFactorChoices.TYPE_4POST]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_width(self):
|
def test_width(self):
|
||||||
|
@ -74,6 +74,61 @@ class LocationTestCase(TestCase):
|
|||||||
self.assertEqual(PowerPanel.objects.get(pk=powerpanel1.pk).site, site_b)
|
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):
|
class RackTestCase(TestCase):
|
||||||
|
|
||||||
@classmethod
|
@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):
|
class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
model = Rack
|
model = Rack
|
||||||
|
|
||||||
@ -380,7 +449,7 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
'role': rackroles[1].pk,
|
'role': rackroles[1].pk,
|
||||||
'serial': '123456',
|
'serial': '123456',
|
||||||
'asset_tag': 'ABCDEF',
|
'asset_tag': 'ABCDEF',
|
||||||
'type': RackTypeChoices.TYPE_CABINET,
|
'form_factor': RackFormFactorChoices.TYPE_CABINET,
|
||||||
'width': RackWidthChoices.WIDTH_19IN,
|
'width': RackWidthChoices.WIDTH_19IN,
|
||||||
'u_height': 48,
|
'u_height': 48,
|
||||||
'desc_units': False,
|
'desc_units': False,
|
||||||
@ -416,7 +485,7 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
'status': RackStatusChoices.STATUS_DEPRECATED,
|
'status': RackStatusChoices.STATUS_DEPRECATED,
|
||||||
'role': rackroles[1].pk,
|
'role': rackroles[1].pk,
|
||||||
'serial': '654321',
|
'serial': '654321',
|
||||||
'type': RackTypeChoices.TYPE_4POST,
|
'form_factor': RackFormFactorChoices.TYPE_4POST,
|
||||||
'width': RackWidthChoices.WIDTH_23IN,
|
'width': RackWidthChoices.WIDTH_23IN,
|
||||||
'u_height': 49,
|
'u_height': 49,
|
||||||
'desc_units': True,
|
'desc_units': True,
|
||||||
|
@ -63,6 +63,14 @@ urlpatterns = [
|
|||||||
path('racks/delete/', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'),
|
path('racks/delete/', views.RackBulkDeleteView.as_view(), name='rack_bulk_delete'),
|
||||||
path('racks/<int:pk>/', include(get_model_urls('dcim', 'rack'))),
|
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
|
# Manufacturers
|
||||||
path('manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'),
|
path('manufacturers/', views.ManufacturerListView.as_view(), name='manufacturer_list'),
|
||||||
path('manufacturers/add/', views.ManufacturerEditView.as_view(), name='manufacturer_add'),
|
path('manufacturers/add/', views.ManufacturerEditView.as_view(), name='manufacturer_add'),
|
||||||
|
@ -579,6 +579,56 @@ class RackRoleBulkDeleteView(generic.BulkDeleteView):
|
|||||||
table = tables.RackRoleTable
|
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
|
# Racks
|
||||||
#
|
#
|
||||||
|
@ -1184,6 +1184,7 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
'rack',
|
'rack',
|
||||||
'rackreservation',
|
'rackreservation',
|
||||||
'rackrole',
|
'rackrole',
|
||||||
|
'racktype',
|
||||||
'rearport',
|
'rearport',
|
||||||
'region',
|
'region',
|
||||||
'rir',
|
'rir',
|
||||||
|
@ -24,6 +24,7 @@ ORGANIZATION_MENU = Menu(
|
|||||||
label=_('Racks'),
|
label=_('Racks'),
|
||||||
items=(
|
items=(
|
||||||
get_model_item('dcim', 'rack', _('Racks')),
|
get_model_item('dcim', 'rack', _('Racks')),
|
||||||
|
get_model_item('dcim', 'racktype', _('Rack Types')),
|
||||||
get_model_item('dcim', 'rackrole', _('Rack Roles')),
|
get_model_item('dcim', 'rackrole', _('Rack Roles')),
|
||||||
get_model_item('dcim', 'rackreservation', _('Reservations')),
|
get_model_item('dcim', 'rackreservation', _('Reservations')),
|
||||||
MenuItem(
|
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>
|
@ -14,9 +14,7 @@
|
|||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Region" %}</th>
|
<th scope="row">{% trans "Region" %}</th>
|
||||||
<td>
|
<td>{% nested_tree object.site.region %}</td>
|
||||||
{% nested_tree object.site.region %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Site" %}</th>
|
<th scope="row">{% trans "Site" %}</th>
|
||||||
@ -43,6 +41,10 @@
|
|||||||
<th scope="row">{% trans "Status" %}</th>
|
<th scope="row">{% trans "Status" %}</th>
|
||||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Rack Type" %}</th>
|
||||||
|
<td>{{ object.rack_type|linkify|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Role" %}</th>
|
<th scope="row">{% trans "Role" %}</th>
|
||||||
<td>{{ object.role|linkify|placeholder }}</td>
|
<td>{{ object.role|linkify|placeholder }}</td>
|
||||||
@ -69,63 +71,11 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
{% include 'dcim/inc/panels/racktype_dimensions.html' %}
|
||||||
|
{% include 'dcim/inc/panels/racktype_numbering.html' %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">{% trans "Dimensions" %}</h5>
|
<h5 class="card-header">{% trans "Weight" %}</h5>
|
||||||
<table class="table table-hover attr-table">
|
<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>
|
<tr>
|
||||||
<th scope="row">{% trans "Rack Weight" %}</th>
|
<th scope="row">{% trans "Rack Weight" %}</th>
|
||||||
<td>
|
<td>
|
||||||
|
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