mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 09:51:22 -06:00
9654 device weight (#10448)
* 9654 add weight fields to devices * 9654 changes from code review * 9654 change _abs_weight to grams * Resolve migrations conflict * 9654 code-review changes * 9654 total weight on devices * Misc cleanup Co-authored-by: Jeremy Stretch <jstretch@ns1.com>
This commit is contained in:
parent
af8bb0c4b9
commit
204c10c053
@ -41,6 +41,10 @@ Indicates whether this is a parent type (capable of housing child devices), a ch
|
|||||||
|
|
||||||
The default direction in which airflow circulates within the device chassis. This may be configured differently for instantiated devices (e.g. because of different fan modules).
|
The default direction in which airflow circulates within the device chassis. This may be configured differently for instantiated devices (e.g. because of different fan modules).
|
||||||
|
|
||||||
|
### Weight
|
||||||
|
|
||||||
|
The numeric weight of the device, including a unit designation (e.g. 10 kilograms or 20 pounds).
|
||||||
|
|
||||||
### Front & Rear Images
|
### Front & Rear Images
|
||||||
|
|
||||||
Users can upload illustrations of the device's front and rear panels. If present, these will be used to render the device in [rack](./rack.md) elevation diagrams.
|
Users can upload illustrations of the device's front and rear panels. If present, these will be used to render the device in [rack](./rack.md) elevation diagrams.
|
||||||
|
@ -35,3 +35,7 @@ The model number assigned to this module type by its manufacturer. Must be uniqu
|
|||||||
### Part Number
|
### Part Number
|
||||||
|
|
||||||
An alternative part number to uniquely identify the module type.
|
An alternative part number to uniquely identify the module type.
|
||||||
|
|
||||||
|
### Weight
|
||||||
|
|
||||||
|
The numeric weight of the module, including a unit designation (e.g. 3 kilograms or 1 pound).
|
||||||
|
@ -65,6 +65,10 @@ The height of the rack, measured in units.
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
### Weight
|
||||||
|
|
||||||
|
The numeric weight of the rack, including a unit designation (e.g. 10 kilograms or 20 pounds).
|
||||||
|
|
||||||
### Descending Units
|
### Descending Units
|
||||||
|
|
||||||
If selected, the rack's elevation will display unit 1 at the top of the rack. (Most racks use asceneding numbering, with unit 1 assigned to the bottommost position.)
|
If selected, the rack's elevation will display unit 1 at the top of the rack. (Most racks use asceneding numbering, with unit 1 assigned to the bottommost position.)
|
||||||
|
@ -201,6 +201,7 @@ class RackSerializer(NetBoxModelSerializer):
|
|||||||
default=None)
|
default=None)
|
||||||
width = ChoiceField(choices=RackWidthChoices, required=False)
|
width = ChoiceField(choices=RackWidthChoices, required=False)
|
||||||
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False)
|
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False)
|
||||||
|
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False)
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = serializers.IntegerField(read_only=True)
|
||||||
powerfeed_count = serializers.IntegerField(read_only=True)
|
powerfeed_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
@ -208,8 +209,9 @@ class RackSerializer(NetBoxModelSerializer):
|
|||||||
model = Rack
|
model = Rack
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial',
|
'id', 'url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial',
|
||||||
'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
|
'asset_tag', 'type', 'width', 'u_height', 'weight', 'weight_unit', 'desc_units', 'outer_width',
|
||||||
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count',
|
'outer_depth', 'outer_unit', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
|
||||||
|
'powerfeed_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -315,27 +317,29 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
|
|||||||
)
|
)
|
||||||
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
|
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
|
||||||
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False)
|
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False)
|
||||||
|
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False)
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
'id', 'url', 'display', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
|
||||||
'subdevice_role', 'airflow', 'front_image', 'rear_image', 'comments', 'tags', 'custom_fields', 'created',
|
'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image', 'comments', 'tags',
|
||||||
'last_updated', 'device_count',
|
'custom_fields', 'created', 'last_updated', 'device_count',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ModuleTypeSerializer(NetBoxModelSerializer):
|
class ModuleTypeSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
|
||||||
manufacturer = NestedManufacturerSerializer()
|
manufacturer = NestedManufacturerSerializer()
|
||||||
|
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False)
|
||||||
# module_count = serializers.IntegerField(read_only=True)
|
# module_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ModuleType
|
model = ModuleType
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'manufacturer', 'model', 'part_number', 'comments', 'tags', 'custom_fields',
|
'id', 'url', 'display', 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'comments', 'tags',
|
||||||
'created', 'last_updated',
|
'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -1314,6 +1314,24 @@ class CableLengthUnitChoices(ChoiceSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WeightUnitChoices(ChoiceSet):
|
||||||
|
|
||||||
|
# Metric
|
||||||
|
UNIT_KILOGRAM = 'kg'
|
||||||
|
UNIT_GRAM = 'g'
|
||||||
|
|
||||||
|
# Imperial
|
||||||
|
UNIT_POUND = 'lb'
|
||||||
|
UNIT_OUNCE = 'oz'
|
||||||
|
|
||||||
|
CHOICES = (
|
||||||
|
(UNIT_KILOGRAM, 'Kilograms'),
|
||||||
|
(UNIT_GRAM, 'Grams'),
|
||||||
|
(UNIT_POUND, 'Pounds'),
|
||||||
|
(UNIT_OUNCE, 'Ounces'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# CableTerminations
|
# CableTerminations
|
||||||
#
|
#
|
||||||
|
@ -320,7 +320,7 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
|
|||||||
model = Rack
|
model = Rack
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'facility_id', 'asset_tag', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
|
'id', 'name', 'facility_id', 'asset_tag', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
|
||||||
'outer_unit',
|
'outer_unit', 'weight', 'weight_unit'
|
||||||
]
|
]
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
@ -482,7 +482,7 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
|
'id', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit'
|
||||||
]
|
]
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
@ -576,7 +576,7 @@ class ModuleTypeFilterSet(NetBoxModelFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ModuleType
|
model = ModuleType
|
||||||
fields = ['id', 'model', 'part_number']
|
fields = ['id', 'model', 'part_number', 'weight', 'weight_unit']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
@ -285,15 +285,26 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
widget=SmallTextarea,
|
widget=SmallTextarea,
|
||||||
label='Comments'
|
label='Comments'
|
||||||
)
|
)
|
||||||
|
weight = forms.DecimalField(
|
||||||
|
min_value=0,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
weight_unit = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(WeightUnitChoices),
|
||||||
|
required=False,
|
||||||
|
initial='',
|
||||||
|
widget=StaticSelect()
|
||||||
|
)
|
||||||
|
|
||||||
model = Rack
|
model = Rack
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Rack', ('status', 'role', 'tenant', 'serial', 'asset_tag')),
|
('Rack', ('status', 'role', 'tenant', 'serial', 'asset_tag')),
|
||||||
('Location', ('region', 'site_group', 'site', 'location')),
|
('Location', ('region', 'site_group', 'site', 'location')),
|
||||||
('Hardware', ('type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit')),
|
('Hardware', ('type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit')),
|
||||||
|
('Weight', ('weight', 'weight_unit')),
|
||||||
)
|
)
|
||||||
nullable_fields = (
|
nullable_fields = (
|
||||||
'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'comments',
|
'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'comments', 'weight', 'weight_unit'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -355,12 +366,23 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
required=False,
|
required=False,
|
||||||
widget=StaticSelect()
|
widget=StaticSelect()
|
||||||
)
|
)
|
||||||
|
weight = forms.DecimalField(
|
||||||
|
min_value=0,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
weight_unit = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(WeightUnitChoices),
|
||||||
|
required=False,
|
||||||
|
initial='',
|
||||||
|
widget=StaticSelect()
|
||||||
|
)
|
||||||
|
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('manufacturer', 'part_number', 'u_height', 'is_full_depth', 'airflow')),
|
('Device Type', ('manufacturer', 'part_number', 'u_height', 'is_full_depth', 'airflow')),
|
||||||
|
('Weight', ('weight', 'weight_unit')),
|
||||||
)
|
)
|
||||||
nullable_fields = ('part_number', 'airflow')
|
nullable_fields = ('part_number', 'airflow', 'weight', 'weight_unit')
|
||||||
|
|
||||||
|
|
||||||
class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
|
class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
@ -371,12 +393,23 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
part_number = forms.CharField(
|
part_number = forms.CharField(
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
weight = forms.DecimalField(
|
||||||
|
min_value=0,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
weight_unit = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(WeightUnitChoices),
|
||||||
|
required=False,
|
||||||
|
initial='',
|
||||||
|
widget=StaticSelect()
|
||||||
|
)
|
||||||
|
|
||||||
model = ModuleType
|
model = ModuleType
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('manufacturer', 'part_number')),
|
('Module Type', ('manufacturer', 'part_number')),
|
||||||
|
('Weight', ('weight', 'weight_unit')),
|
||||||
)
|
)
|
||||||
nullable_fields = ('part_number',)
|
nullable_fields = ('part_number', 'weight', 'weight_unit')
|
||||||
|
|
||||||
|
|
||||||
class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm):
|
class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
@ -553,17 +586,6 @@ class CableBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
'type', 'status', 'tenant', 'label', 'color', 'length',
|
'type', 'status', 'tenant', 'label', 'color', 'length',
|
||||||
)
|
)
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
super().clean()
|
|
||||||
|
|
||||||
# Validate length/unit
|
|
||||||
length = self.cleaned_data.get('length')
|
|
||||||
length_unit = self.cleaned_data.get('length_unit')
|
|
||||||
if length and not length_unit:
|
|
||||||
raise forms.ValidationError({
|
|
||||||
'length_unit': "Must specify a unit when setting length"
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm):
|
class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
domain = forms.CharField(
|
domain = forms.CharField(
|
||||||
|
@ -228,6 +228,7 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
|
|||||||
('Hardware', ('type', 'width', 'serial', 'asset_tag')),
|
('Hardware', ('type', 'width', 'serial', 'asset_tag')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
('Contacts', ('contact', 'contact_role', 'contact_group')),
|
||||||
|
('Weight', ('weight', 'weight_unit')),
|
||||||
)
|
)
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
@ -281,6 +282,13 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
|
|||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
weight = forms.DecimalField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
weight_unit = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(WeightUnitChoices),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RackElevationFilterForm(RackFilterForm):
|
class RackElevationFilterForm(RackFilterForm):
|
||||||
@ -370,6 +378,7 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
|
|||||||
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
|
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
|
||||||
'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items',
|
'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items',
|
||||||
)),
|
)),
|
||||||
|
('Weight', ('weight', 'weight_unit')),
|
||||||
)
|
)
|
||||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
@ -465,6 +474,13 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
weight = forms.DecimalField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
weight_unit = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(WeightUnitChoices),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
|
class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
|
||||||
@ -476,6 +492,7 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
|
|||||||
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
|
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
|
||||||
'pass_through_ports',
|
'pass_through_ports',
|
||||||
)),
|
)),
|
||||||
|
('Weight', ('weight', 'weight_unit')),
|
||||||
)
|
)
|
||||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Manufacturer.objects.all(),
|
queryset=Manufacturer.objects.all(),
|
||||||
@ -529,6 +546,13 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
weight = forms.DecimalField(
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
weight_unit = forms.ChoiceField(
|
||||||
|
choices=add_blank_choice(WeightUnitChoices),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DeviceRoleFilterForm(NetBoxModelFilterSetForm):
|
class DeviceRoleFilterForm(NetBoxModelFilterSetForm):
|
||||||
|
@ -260,7 +260,7 @@ class RackForm(TenancyForm, NetBoxModelForm):
|
|||||||
fields = [
|
fields = [
|
||||||
'region', 'site_group', 'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status',
|
'region', 'site_group', 'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status',
|
||||||
'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
|
'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
|
||||||
'outer_unit', 'comments', 'tags',
|
'outer_unit', 'weight', 'weight_unit', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'site': "The site at which the rack exists",
|
'site': "The site at which the rack exists",
|
||||||
@ -273,6 +273,7 @@ class RackForm(TenancyForm, NetBoxModelForm):
|
|||||||
'type': StaticSelect(),
|
'type': StaticSelect(),
|
||||||
'width': StaticSelect(),
|
'width': StaticSelect(),
|
||||||
'outer_unit': StaticSelect(),
|
'outer_unit': StaticSelect(),
|
||||||
|
'weight_unit': StaticSelect(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -363,6 +364,7 @@ class DeviceTypeForm(NetBoxModelForm):
|
|||||||
('Chassis', (
|
('Chassis', (
|
||||||
'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
|
'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
|
||||||
)),
|
)),
|
||||||
|
('Attributes', ('weight', 'weight_unit')),
|
||||||
('Images', ('front_image', 'rear_image')),
|
('Images', ('front_image', 'rear_image')),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -370,7 +372,7 @@ class DeviceTypeForm(NetBoxModelForm):
|
|||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = [
|
fields = [
|
||||||
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
|
'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
|
||||||
'front_image', 'rear_image', 'comments', 'tags',
|
'weight', 'weight_unit', 'front_image', 'rear_image', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'airflow': StaticSelect(),
|
'airflow': StaticSelect(),
|
||||||
@ -380,7 +382,8 @@ class DeviceTypeForm(NetBoxModelForm):
|
|||||||
}),
|
}),
|
||||||
'rear_image': ClearableFileInput(attrs={
|
'rear_image': ClearableFileInput(attrs={
|
||||||
'accept': DEVICETYPE_IMAGE_FORMATS
|
'accept': DEVICETYPE_IMAGE_FORMATS
|
||||||
})
|
}),
|
||||||
|
'weight_unit': StaticSelect(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -392,16 +395,20 @@ class ModuleTypeForm(NetBoxModelForm):
|
|||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Module Type', (
|
('Module Type', (
|
||||||
'manufacturer', 'model', 'part_number', 'tags',
|
'manufacturer', 'model', 'part_number', 'tags', 'weight', 'weight_unit'
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ModuleType
|
model = ModuleType
|
||||||
fields = [
|
fields = [
|
||||||
'manufacturer', 'model', 'part_number', 'comments', 'tags',
|
'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
widgets = {
|
||||||
|
'weight_unit': StaticSelect(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeviceRoleForm(NetBoxModelForm):
|
class DeviceRoleForm(NetBoxModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
@ -211,6 +211,9 @@ class DeviceTypeType(NetBoxObjectType):
|
|||||||
def resolve_airflow(self, info):
|
def resolve_airflow(self, info):
|
||||||
return self.airflow or None
|
return self.airflow or None
|
||||||
|
|
||||||
|
def resolve_weight_unit(self, info):
|
||||||
|
return self.weight_unit or None
|
||||||
|
|
||||||
|
|
||||||
class FrontPortType(ComponentObjectType, CabledObjectMixin):
|
class FrontPortType(ComponentObjectType, CabledObjectMixin):
|
||||||
|
|
||||||
@ -328,6 +331,9 @@ class ModuleTypeType(NetBoxObjectType):
|
|||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
filterset_class = filtersets.ModuleTypeFilterSet
|
filterset_class = filtersets.ModuleTypeFilterSet
|
||||||
|
|
||||||
|
def resolve_weight_unit(self, info):
|
||||||
|
return self.weight_unit or None
|
||||||
|
|
||||||
|
|
||||||
class PlatformType(OrganizationalObjectType):
|
class PlatformType(OrganizationalObjectType):
|
||||||
|
|
||||||
@ -416,6 +422,9 @@ class RackType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
|
|||||||
def resolve_outer_unit(self, info):
|
def resolve_outer_unit(self, info):
|
||||||
return self.outer_unit or None
|
return self.outer_unit or None
|
||||||
|
|
||||||
|
def resolve_weight_unit(self, info):
|
||||||
|
return self.weight_unit or None
|
||||||
|
|
||||||
|
|
||||||
class RackReservationType(NetBoxObjectType):
|
class RackReservationType(NetBoxObjectType):
|
||||||
|
|
||||||
|
@ -0,0 +1,58 @@
|
|||||||
|
# Generated by Django 4.0.7 on 2022-09-23 01:01
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0162_unique_constraints'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='_abs_weight',
|
||||||
|
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='weight',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='devicetype',
|
||||||
|
name='weight_unit',
|
||||||
|
field=models.CharField(blank=True, max_length=50),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='moduletype',
|
||||||
|
name='_abs_weight',
|
||||||
|
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='moduletype',
|
||||||
|
name='weight',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='moduletype',
|
||||||
|
name='weight_unit',
|
||||||
|
field=models.CharField(blank=True, max_length=50),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rack',
|
||||||
|
name='_abs_weight',
|
||||||
|
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rack',
|
||||||
|
name='weight',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rack',
|
||||||
|
name='weight_unit',
|
||||||
|
field=models.CharField(blank=True, max_length=50),
|
||||||
|
),
|
||||||
|
]
|
@ -1,7 +1,8 @@
|
|||||||
import decimal
|
import decimal
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
from functools import cached_property
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@ -21,6 +22,7 @@ from netbox.models import OrganizationalModel, NetBoxModel
|
|||||||
from utilities.choices import ColorChoices
|
from utilities.choices import ColorChoices
|
||||||
from utilities.fields import ColorField, NaturalOrderingField
|
from utilities.fields import ColorField, NaturalOrderingField
|
||||||
from .device_components import *
|
from .device_components import *
|
||||||
|
from .mixins import WeightMixin
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -71,7 +73,7 @@ class Manufacturer(OrganizationalModel):
|
|||||||
return reverse('dcim:manufacturer', args=[self.pk])
|
return reverse('dcim:manufacturer', args=[self.pk])
|
||||||
|
|
||||||
|
|
||||||
class DeviceType(NetBoxModel):
|
class DeviceType(NetBoxModel, WeightMixin):
|
||||||
"""
|
"""
|
||||||
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
|
A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
|
||||||
well as high-level functional role(s).
|
well as high-level functional role(s).
|
||||||
@ -139,7 +141,7 @@ class DeviceType(NetBoxModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
clone_fields = (
|
clone_fields = (
|
||||||
'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow',
|
'manufacturer', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -315,7 +317,7 @@ class DeviceType(NetBoxModel):
|
|||||||
return self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD
|
return self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD
|
||||||
|
|
||||||
|
|
||||||
class ModuleType(NetBoxModel):
|
class ModuleType(NetBoxModel, WeightMixin):
|
||||||
"""
|
"""
|
||||||
A ModuleType represents a hardware element that can be installed within a device and which houses additional
|
A ModuleType represents a hardware element that can be installed within a device and which houses additional
|
||||||
components; for example, a line card within a chassis-based switch such as the Cisco Catalyst 6500. Like a
|
components; for example, a line card within a chassis-based switch such as the Cisco Catalyst 6500. Like a
|
||||||
@ -344,7 +346,7 @@ class ModuleType(NetBoxModel):
|
|||||||
to='extras.ImageAttachment'
|
to='extras.ImageAttachment'
|
||||||
)
|
)
|
||||||
|
|
||||||
clone_fields = ('manufacturer',)
|
clone_fields = ('manufacturer', 'weight', 'weight_unit',)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('manufacturer', 'model')
|
ordering = ('manufacturer', 'model')
|
||||||
@ -946,6 +948,18 @@ class Device(NetBoxModel, ConfigContextModel):
|
|||||||
def get_status_color(self):
|
def get_status_color(self):
|
||||||
return DeviceStatusChoices.colors.get(self.status)
|
return DeviceStatusChoices.colors.get(self.status)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def total_weight(self):
|
||||||
|
total_weight = sum(
|
||||||
|
module.module_type._abs_weight
|
||||||
|
for module in Module.objects.filter(device=self)
|
||||||
|
.exclude(module_type___abs_weight__isnull=True)
|
||||||
|
.prefetch_related('module_type')
|
||||||
|
)
|
||||||
|
if self.device_type._abs_weight:
|
||||||
|
total_weight += self.device_type._abs_weight
|
||||||
|
return round(total_weight / 1000, 2)
|
||||||
|
|
||||||
|
|
||||||
class Module(NetBoxModel, ConfigContextModel):
|
class Module(NetBoxModel, ConfigContextModel):
|
||||||
"""
|
"""
|
||||||
|
45
netbox/dcim/models/mixins.py
Normal file
45
netbox/dcim/models/mixins.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
from dcim.choices import *
|
||||||
|
from utilities.utils import to_grams
|
||||||
|
|
||||||
|
|
||||||
|
class WeightMixin(models.Model):
|
||||||
|
weight = models.DecimalField(
|
||||||
|
max_digits=8,
|
||||||
|
decimal_places=2,
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
weight_unit = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=WeightUnitChoices,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
# Stores the normalized weight (in grams) for database ordering
|
||||||
|
_abs_weight = models.PositiveBigIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
|
# Store the given weight (if any) in grams for use in database ordering
|
||||||
|
if self.weight and self.weight_unit:
|
||||||
|
self._abs_weight = to_grams(self.weight, self.weight_unit)
|
||||||
|
else:
|
||||||
|
self._abs_weight = None
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
# Validate weight and weight_unit
|
||||||
|
if self.weight is not None and not self.weight_unit:
|
||||||
|
raise ValidationError("Must specify a unit when setting a weight")
|
||||||
|
elif self.weight is None:
|
||||||
|
self.weight_unit = ''
|
@ -1,4 +1,5 @@
|
|||||||
import decimal
|
import decimal
|
||||||
|
from functools import cached_property
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
@ -18,7 +19,8 @@ from utilities.choices import ColorChoices
|
|||||||
from utilities.fields import ColorField, NaturalOrderingField
|
from utilities.fields import ColorField, NaturalOrderingField
|
||||||
from utilities.utils import array_to_string, drange
|
from utilities.utils import array_to_string, drange
|
||||||
from .device_components import PowerPort
|
from .device_components import PowerPort
|
||||||
from .devices import Device
|
from .devices import Device, Module
|
||||||
|
from .mixins import WeightMixin
|
||||||
from .power import PowerFeed
|
from .power import PowerFeed
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -62,7 +64,7 @@ class RackRole(OrganizationalModel):
|
|||||||
return reverse('dcim:rackrole', args=[self.pk])
|
return reverse('dcim:rackrole', args=[self.pk])
|
||||||
|
|
||||||
|
|
||||||
class Rack(NetBoxModel):
|
class Rack(NetBoxModel, WeightMixin):
|
||||||
"""
|
"""
|
||||||
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.
|
||||||
@ -185,7 +187,7 @@ class Rack(NetBoxModel):
|
|||||||
|
|
||||||
clone_fields = (
|
clone_fields = (
|
||||||
'site', 'location', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
|
'site', 'location', 'tenant', 'status', 'role', 'type', 'width', 'u_height', 'desc_units', 'outer_width',
|
||||||
'outer_depth', 'outer_unit',
|
'outer_depth', 'outer_unit', 'weight', 'weight_unit',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -454,6 +456,22 @@ class Rack(NetBoxModel):
|
|||||||
|
|
||||||
return int(allocated_draw / available_power_total * 100)
|
return int(allocated_draw / available_power_total * 100)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def total_weight(self):
|
||||||
|
total_weight = sum(
|
||||||
|
device.device_type._abs_weight
|
||||||
|
for device in self.devices.exclude(device_type___abs_weight__isnull=True).prefetch_related('device_type')
|
||||||
|
)
|
||||||
|
total_weight += sum(
|
||||||
|
module.module_type._abs_weight
|
||||||
|
for module in Module.objects.filter(device__rack=self)
|
||||||
|
.exclude(module_type___abs_weight__isnull=True)
|
||||||
|
.prefetch_related('module_type')
|
||||||
|
)
|
||||||
|
if self._abs_weight:
|
||||||
|
total_weight += self._abs_weight
|
||||||
|
return round(total_weight / 1000, 2)
|
||||||
|
|
||||||
|
|
||||||
class RackReservation(NetBoxModel):
|
class RackReservation(NetBoxModel):
|
||||||
"""
|
"""
|
||||||
|
@ -5,7 +5,7 @@ from dcim.models import (
|
|||||||
InventoryItemTemplate, Manufacturer, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
|
InventoryItemTemplate, Manufacturer, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
|
||||||
)
|
)
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS
|
from .template_code import MODULAR_COMPONENT_TEMPLATE_BUTTONS, DEVICE_WEIGHT
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ConsolePortTemplateTable',
|
'ConsolePortTemplateTable',
|
||||||
@ -85,12 +85,16 @@ class DeviceTypeTable(NetBoxTable):
|
|||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:devicetype_list'
|
url_name='dcim:devicetype_list'
|
||||||
)
|
)
|
||||||
|
weight = columns.TemplateColumn(
|
||||||
|
template_code=DEVICE_WEIGHT,
|
||||||
|
order_by=('_abs_weight', 'weight_unit')
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = DeviceType
|
model = DeviceType
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
'pk', 'id', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
|
||||||
'airflow', 'comments', 'instance_count', 'tags', 'created', 'last_updated',
|
'airflow', 'weight', 'comments', 'instance_count', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count',
|
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count',
|
||||||
|
@ -2,6 +2,7 @@ import django_tables2 as tables
|
|||||||
|
|
||||||
from dcim.models import Module, ModuleType
|
from dcim.models import Module, ModuleType
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
|
from .template_code import DEVICE_WEIGHT
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'ModuleTable',
|
'ModuleTable',
|
||||||
@ -26,11 +27,15 @@ class ModuleTypeTable(NetBoxTable):
|
|||||||
tags = columns.TagColumn(
|
tags = columns.TagColumn(
|
||||||
url_name='dcim:moduletype_list'
|
url_name='dcim:moduletype_list'
|
||||||
)
|
)
|
||||||
|
weight = columns.TemplateColumn(
|
||||||
|
template_code=DEVICE_WEIGHT,
|
||||||
|
order_by=('_abs_weight', 'weight_unit')
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = ModuleType
|
model = ModuleType
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'model', 'manufacturer', 'part_number', 'comments', 'tags',
|
'pk', 'id', 'model', 'manufacturer', 'part_number', 'weight', 'comments', 'tags',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'model', 'manufacturer', 'part_number',
|
'pk', 'model', 'manufacturer', 'part_number',
|
||||||
|
@ -4,6 +4,7 @@ from django_tables2.utils import Accessor
|
|||||||
from dcim.models import Rack, RackReservation, RackRole
|
from dcim.models import Rack, RackReservation, RackRole
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.tables import TenancyColumnsMixin
|
from tenancy.tables import TenancyColumnsMixin
|
||||||
|
from .template_code import DEVICE_WEIGHT
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'RackTable',
|
'RackTable',
|
||||||
@ -82,13 +83,17 @@ class RackTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
template_code="{{ record.outer_depth }} {{ record.outer_unit }}",
|
template_code="{{ record.outer_depth }} {{ record.outer_unit }}",
|
||||||
verbose_name='Outer Depth'
|
verbose_name='Outer Depth'
|
||||||
)
|
)
|
||||||
|
weight = columns.TemplateColumn(
|
||||||
|
template_code=DEVICE_WEIGHT,
|
||||||
|
order_by=('_abs_weight', 'weight_unit')
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role', 'serial', 'asset_tag',
|
'pk', 'id', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'tenant_group', 'role', 'serial',
|
||||||
'type', 'width', 'outer_width', 'outer_depth', 'u_height', 'comments', 'device_count', 'get_utilization',
|
'asset_tag', 'type', 'width', 'outer_width', 'outer_depth', 'u_height', 'weight', 'comments',
|
||||||
'get_power_utilization', 'contacts', 'tags', 'created', 'last_updated',
|
'device_count', 'get_utilization', 'get_power_utilization', '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',
|
||||||
|
@ -15,6 +15,11 @@ CABLE_LENGTH = """
|
|||||||
{% if record.length %}{{ record.length|simplify_decimal }} {{ record.length_unit }}{% endif %}
|
{% if record.length %}{{ record.length|simplify_decimal }} {{ record.length_unit }}{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
DEVICE_WEIGHT = """
|
||||||
|
{% load helpers %}
|
||||||
|
{% if record.weight %}{{ record.weight|simplify_decimal }} {{ record.weight_unit }}{% endif %}
|
||||||
|
"""
|
||||||
|
|
||||||
DEVICE_LINK = """
|
DEVICE_LINK = """
|
||||||
<a href="{% url 'dcim:device' pk=record.pk %}">
|
<a href="{% url 'dcim:device' pk=record.pk %}">
|
||||||
{{ record.name|default:'<span class="badge bg-info">Unnamed device</span>' }}
|
{{ record.name|default:'<span class="badge bg-info">Unnamed device</span>' }}
|
||||||
|
@ -409,9 +409,9 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Tenant.objects.bulk_create(tenants)
|
Tenant.objects.bulk_create(tenants)
|
||||||
|
|
||||||
racks = (
|
racks = (
|
||||||
Rack(name='Rack 1', facility_id='rack-1', site=sites[0], location=locations[0], tenant=tenants[0], status=RackStatusChoices.STATUS_ACTIVE, role=rack_roles[0], serial='ABC', asset_tag='1001', type=RackTypeChoices.TYPE_2POST, width=RackWidthChoices.WIDTH_19IN, u_height=42, desc_units=False, outer_width=100, outer_depth=100, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER),
|
Rack(name='Rack 1', facility_id='rack-1', site=sites[0], location=locations[0], tenant=tenants[0], status=RackStatusChoices.STATUS_ACTIVE, role=rack_roles[0], serial='ABC', asset_tag='1001', type=RackTypeChoices.TYPE_2POST, width=RackWidthChoices.WIDTH_19IN, u_height=42, desc_units=False, outer_width=100, outer_depth=100, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER, weight=10, weight_unit=WeightUnitChoices.UNIT_POUND),
|
||||||
Rack(name='Rack 2', facility_id='rack-2', site=sites[1], location=locations[1], tenant=tenants[1], status=RackStatusChoices.STATUS_PLANNED, role=rack_roles[1], serial='DEF', asset_tag='1002', type=RackTypeChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_21IN, u_height=43, desc_units=False, outer_width=200, outer_depth=200, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER),
|
Rack(name='Rack 2', facility_id='rack-2', site=sites[1], location=locations[1], tenant=tenants[1], status=RackStatusChoices.STATUS_PLANNED, role=rack_roles[1], serial='DEF', asset_tag='1002', type=RackTypeChoices.TYPE_4POST, width=RackWidthChoices.WIDTH_21IN, u_height=43, desc_units=False, outer_width=200, outer_depth=200, outer_unit=RackDimensionUnitChoices.UNIT_MILLIMETER, weight=20, weight_unit=WeightUnitChoices.UNIT_POUND),
|
||||||
Rack(name='Rack 3', facility_id='rack-3', site=sites[2], location=locations[2], tenant=tenants[2], status=RackStatusChoices.STATUS_RESERVED, role=rack_roles[2], serial='GHI', asset_tag='1003', type=RackTypeChoices.TYPE_CABINET, width=RackWidthChoices.WIDTH_23IN, u_height=44, desc_units=True, outer_width=300, outer_depth=300, outer_unit=RackDimensionUnitChoices.UNIT_INCH),
|
Rack(name='Rack 3', facility_id='rack-3', site=sites[2], location=locations[2], tenant=tenants[2], status=RackStatusChoices.STATUS_RESERVED, role=rack_roles[2], serial='GHI', asset_tag='1003', type=RackTypeChoices.TYPE_CABINET, width=RackWidthChoices.WIDTH_23IN, u_height=44, desc_units=True, outer_width=300, outer_depth=300, outer_unit=RackDimensionUnitChoices.UNIT_INCH, weight=30, weight_unit=WeightUnitChoices.UNIT_KILOGRAM),
|
||||||
)
|
)
|
||||||
Rack.objects.bulk_create(racks)
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
@ -517,6 +517,14 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
|
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
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_weight_unit(self):
|
||||||
|
params = {'weight_unit': WeightUnitChoices.UNIT_POUND}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = RackReservation.objects.all()
|
queryset = RackReservation.objects.all()
|
||||||
@ -688,9 +696,9 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Manufacturer.objects.bulk_create(manufacturers)
|
Manufacturer.objects.bulk_create(manufacturers)
|
||||||
|
|
||||||
device_types = (
|
device_types = (
|
||||||
DeviceType(manufacturer=manufacturers[0], model='Model 1', slug='model-1', part_number='Part Number 1', u_height=1, is_full_depth=True, front_image='front.png', rear_image='rear.png'),
|
DeviceType(manufacturer=manufacturers[0], model='Model 1', slug='model-1', part_number='Part Number 1', u_height=1, is_full_depth=True, front_image='front.png', rear_image='rear.png', weight=10, weight_unit=WeightUnitChoices.UNIT_POUND),
|
||||||
DeviceType(manufacturer=manufacturers[1], model='Model 2', slug='model-2', part_number='Part Number 2', u_height=2, is_full_depth=True, subdevice_role=SubdeviceRoleChoices.ROLE_PARENT, airflow=DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR),
|
DeviceType(manufacturer=manufacturers[1], model='Model 2', slug='model-2', part_number='Part Number 2', u_height=2, is_full_depth=True, subdevice_role=SubdeviceRoleChoices.ROLE_PARENT, airflow=DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR, weight=20, weight_unit=WeightUnitChoices.UNIT_POUND),
|
||||||
DeviceType(manufacturer=manufacturers[2], model='Model 3', slug='model-3', part_number='Part Number 3', u_height=3, is_full_depth=False, subdevice_role=SubdeviceRoleChoices.ROLE_CHILD, airflow=DeviceAirflowChoices.AIRFLOW_REAR_TO_FRONT),
|
DeviceType(manufacturer=manufacturers[2], model='Model 3', slug='model-3', part_number='Part Number 3', u_height=3, is_full_depth=False, subdevice_role=SubdeviceRoleChoices.ROLE_CHILD, airflow=DeviceAirflowChoices.AIRFLOW_REAR_TO_FRONT, weight=30, weight_unit=WeightUnitChoices.UNIT_KILOGRAM),
|
||||||
)
|
)
|
||||||
DeviceType.objects.bulk_create(device_types)
|
DeviceType.objects.bulk_create(device_types)
|
||||||
|
|
||||||
@ -839,6 +847,14 @@ class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'inventory_items': 'false'}
|
params = {'inventory_items': 'false'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
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_weight_unit(self):
|
||||||
|
params = {'weight_unit': WeightUnitChoices.UNIT_POUND}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = ModuleType.objects.all()
|
queryset = ModuleType.objects.all()
|
||||||
@ -855,9 +871,9 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Manufacturer.objects.bulk_create(manufacturers)
|
Manufacturer.objects.bulk_create(manufacturers)
|
||||||
|
|
||||||
module_types = (
|
module_types = (
|
||||||
ModuleType(manufacturer=manufacturers[0], model='Model 1', part_number='Part Number 1'),
|
ModuleType(manufacturer=manufacturers[0], model='Model 1', part_number='Part Number 1', weight=10, weight_unit=WeightUnitChoices.UNIT_POUND),
|
||||||
ModuleType(manufacturer=manufacturers[1], model='Model 2', part_number='Part Number 2'),
|
ModuleType(manufacturer=manufacturers[1], model='Model 2', part_number='Part Number 2', weight=20, weight_unit=WeightUnitChoices.UNIT_POUND),
|
||||||
ModuleType(manufacturer=manufacturers[2], model='Model 3', part_number='Part Number 3'),
|
ModuleType(manufacturer=manufacturers[2], model='Model 3', part_number='Part Number 3', weight=30, weight_unit=WeightUnitChoices.UNIT_KILOGRAM),
|
||||||
)
|
)
|
||||||
ModuleType.objects.bulk_create(module_types)
|
ModuleType.objects.bulk_create(module_types)
|
||||||
|
|
||||||
@ -943,6 +959,14 @@ class ModuleTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'pass_through_ports': 'false'}
|
params = {'pass_through_ports': 'false'}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
def test_weight(self):
|
||||||
|
params = {'weight': [10, 20]}
|
||||||
|
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 ConsolePortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class ConsolePortTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = ConsolePortTemplate.objects.all()
|
queryset = ConsolePortTemplate.objects.all()
|
||||||
|
@ -35,6 +35,16 @@
|
|||||||
<td>Full Depth</td>
|
<td>Full Depth</td>
|
||||||
<td>{% checkmark object.is_full_depth %}</td>
|
<td>{% checkmark object.is_full_depth %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Weight</td>
|
||||||
|
<td>
|
||||||
|
{% if object.weight %}
|
||||||
|
{{ object.weight|floatformat }} {{ object.get_weight_unit_display }}
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Parent/Child</td>
|
<td>Parent/Child</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -22,6 +22,16 @@
|
|||||||
<td>Part Number</td>
|
<td>Part Number</td>
|
||||||
<td>{{ object.part_number|placeholder }}</td>
|
<td>{{ object.part_number|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Weight</td>
|
||||||
|
<td>
|
||||||
|
{% if object.weight %}
|
||||||
|
{{ object.weight|floatformat }} {{ object.get_weight_unit_display }}
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Instances</td>
|
<td>Instances</td>
|
||||||
<td><a href="{% url 'dcim:module_list' %}?module_type_id={{ object.pk }}">{{ instance_count }}</a></td>
|
<td><a href="{% url 'dcim:module_list' %}?module_type_id={{ object.pk }}">{{ instance_count }}</a></td>
|
||||||
|
@ -104,9 +104,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">Dimensions</h5>
|
||||||
Dimensions
|
|
||||||
</h5>
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
@ -147,6 +145,20 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Rack Weight</th>
|
||||||
|
<td>
|
||||||
|
{% if object.weight %}
|
||||||
|
{{ object.weight|floatformat }} {{ object.get_weight_unit_display }}
|
||||||
|
{% else %}
|
||||||
|
{{ ''|placeholder }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Total Weight</th>
|
||||||
|
<td>{{ object.total_weight|floatformat }} Kilograms</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -186,6 +198,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% include 'inc/panels/image_attachments.html' %}
|
{% include 'inc/panels/image_attachments.html' %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">
|
||||||
|
@ -57,6 +57,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{% render_field form.desc_units %}
|
{% render_field form.desc_units %}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field-group my-5">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<h5 class="offset-sm-3">Weight</h5>
|
||||||
|
</div>
|
||||||
|
{% render_field form.weight %}
|
||||||
|
{% render_field form.weight_unit %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{% if form.custom_fields %}
|
{% if form.custom_fields %}
|
||||||
<div class="field-group my-5">
|
<div class="field-group my-5">
|
||||||
|
@ -12,7 +12,7 @@ from django.http import QueryDict
|
|||||||
from jinja2.sandbox import SandboxedEnvironment
|
from jinja2.sandbox import SandboxedEnvironment
|
||||||
from mptt.models import MPTTModel
|
from mptt.models import MPTTModel
|
||||||
|
|
||||||
from dcim.choices import CableLengthUnitChoices
|
from dcim.choices import CableLengthUnitChoices, WeightUnitChoices
|
||||||
from extras.plugins import PluginConfig
|
from extras.plugins import PluginConfig
|
||||||
from extras.utils import is_taggable
|
from extras.utils import is_taggable
|
||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
@ -270,6 +270,31 @@ def to_meters(length, unit):
|
|||||||
raise ValueError(f"Unknown unit {unit}. Must be 'km', 'm', 'cm', 'mi', 'ft', or 'in'.")
|
raise ValueError(f"Unknown unit {unit}. Must be 'km', 'm', 'cm', 'mi', 'ft', or 'in'.")
|
||||||
|
|
||||||
|
|
||||||
|
def to_grams(weight, unit):
|
||||||
|
"""
|
||||||
|
Convert the given weight to kilograms.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if weight < 0:
|
||||||
|
raise ValueError("Weight must be a positive number")
|
||||||
|
except TypeError:
|
||||||
|
raise TypeError(f"Invalid value '{weight}' for weight (must be a number)")
|
||||||
|
|
||||||
|
valid_units = WeightUnitChoices.values()
|
||||||
|
if unit not in valid_units:
|
||||||
|
raise ValueError(f"Unknown unit {unit}. Must be one of the following: {', '.join(valid_units)}")
|
||||||
|
|
||||||
|
if unit == WeightUnitChoices.UNIT_KILOGRAM:
|
||||||
|
return weight * 1000
|
||||||
|
if unit == WeightUnitChoices.UNIT_GRAM:
|
||||||
|
return weight
|
||||||
|
if unit == WeightUnitChoices.UNIT_POUND:
|
||||||
|
return weight * Decimal(453.592)
|
||||||
|
if unit == WeightUnitChoices.UNIT_OUNCE:
|
||||||
|
return weight * Decimal(28.3495)
|
||||||
|
raise ValueError(f"Unknown unit {unit}. Must be 'kg', 'g', 'lb', 'oz'.")
|
||||||
|
|
||||||
|
|
||||||
def render_jinja2(template_code, context):
|
def render_jinja2(template_code, context):
|
||||||
"""
|
"""
|
||||||
Render a Jinja2 template with the provided context. Return the rendered content.
|
Render a Jinja2 template with the provided context. Return the rendered content.
|
||||||
|
Loading…
Reference in New Issue
Block a user