Selaa lähdekoodia

cooling diameter

Arthur 1 viikko sitten
vanhempi
commit
8a7d275343

+ 1 - 1
docs/models/dcim/coolingoutlet.md

@@ -33,7 +33,7 @@ The physical coolant connector type (e.g. UQD, UQDB, QDC, camlock, or threaded N
 
 ### Diameter
 
-The nominal connector diameter.
+The connector diameter, expressed as a numeric value with a selectable unit (millimeters, centimeters, or inches).
 
 ### Cooling Port
 

+ 1 - 1
docs/models/dcim/coolingport.md

@@ -33,7 +33,7 @@ The physical coolant connector type (e.g. UQD, UQDB, QDC, camlock, or threaded N
 
 ### Diameter
 
-The nominal connector diameter.
+The connector diameter, expressed as a numeric value with a selectable unit (millimeters, centimeters, or inches).
 
 ### Maximum Flow
 

+ 9 - 6
netbox/dcim/api/serializers_/device_components.py

@@ -26,6 +26,7 @@ from ipam.models import VLAN
 from netbox.api.fields import ChoiceField, ContentTypeField, RestrictedPrimaryKeyRelatedField, SerializedPKRelatedField
 from netbox.api.gfk_fields import GFKSerializerField
 from netbox.api.serializers import NetBoxModelSerializer
+from netbox.choices import DiameterUnitChoices
 from users.api.serializers_.mixins import OwnerMixin
 from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
 from wireless.api.serializers_.nested import NestedWirelessLinkSerializer
@@ -225,8 +226,8 @@ class CoolingPortSerializer(
         required=False,
         allow_null=True
     )
-    diameter = ChoiceField(
-        choices=CoolingDiameterChoices,
+    diameter_unit = ChoiceField(
+        choices=DiameterUnitChoices,
         allow_blank=True,
         required=False,
         allow_null=True
@@ -236,7 +237,8 @@ class CoolingPortSerializer(
         model = CoolingPort
         fields = [
             'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'connector_type',
-            'diameter', 'maximum_flow', 'heat_capacity', 'description', 'mark_connected', 'cable', 'cable_end',
+            'diameter', 'diameter_unit', 'maximum_flow', 'heat_capacity', 'description', 'mark_connected', 'cable',
+            'cable_end',
             'link_peers', 'link_peers_type', 'connected_endpoints', 'connected_endpoints_type',
             'connected_endpoints_reachable', 'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
         ]
@@ -268,8 +270,8 @@ class CoolingOutletSerializer(
         required=False,
         allow_null=True
     )
-    diameter = ChoiceField(
-        choices=CoolingDiameterChoices,
+    diameter_unit = ChoiceField(
+        choices=DiameterUnitChoices,
         allow_blank=True,
         required=False,
         allow_null=True
@@ -284,7 +286,8 @@ class CoolingOutletSerializer(
         model = CoolingOutlet
         fields = [
             'id', 'url', 'display_url', 'display', 'device', 'module', 'name', 'label', 'type', 'connector_type',
-            'diameter', 'color', 'cooling_port', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
+            'diameter', 'diameter_unit', 'color', 'cooling_port', 'description', 'mark_connected', 'cable', 'cable_end',
+            'link_peers',
             'link_peers_type', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable',
             'owner', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
         ]

+ 7 - 6
netbox/dcim/api/serializers_/devicetype_components.py

@@ -21,6 +21,7 @@ from dcim.models import (
 from netbox.api.fields import ChoiceField, ContentTypeField, RestrictedPrimaryKeyRelatedField
 from netbox.api.gfk_fields import GFKSerializerField
 from netbox.api.serializers import ChangeLogMessageSerializer, ValidatedModelSerializer
+from netbox.choices import DiameterUnitChoices
 from wireless.choices import *
 
 from .base import PortSerializer
@@ -199,8 +200,8 @@ class CoolingPortTemplateSerializer(ComponentTemplateSerializer):
         required=False,
         allow_null=True
     )
-    diameter = ChoiceField(
-        choices=CoolingDiameterChoices,
+    diameter_unit = ChoiceField(
+        choices=DiameterUnitChoices,
         allow_blank=True,
         required=False,
         allow_null=True
@@ -210,7 +211,7 @@ class CoolingPortTemplateSerializer(ComponentTemplateSerializer):
         model = CoolingPortTemplate
         fields = [
             'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'connector_type',
-            'diameter', 'maximum_flow', 'heat_capacity', 'description', 'created', 'last_updated',
+            'diameter', 'diameter_unit', 'maximum_flow', 'heat_capacity', 'description', 'created', 'last_updated',
         ]
         brief_fields = ('id', 'url', 'display', 'name', 'description')
 
@@ -240,8 +241,8 @@ class CoolingOutletTemplateSerializer(ComponentTemplateSerializer):
         required=False,
         allow_null=True
     )
-    diameter = ChoiceField(
-        choices=CoolingDiameterChoices,
+    diameter_unit = ChoiceField(
+        choices=DiameterUnitChoices,
         allow_blank=True,
         required=False,
         allow_null=True
@@ -256,7 +257,7 @@ class CoolingOutletTemplateSerializer(ComponentTemplateSerializer):
         model = CoolingOutletTemplate
         fields = [
             'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'connector_type',
-            'diameter', 'color', 'cooling_port', 'description', 'created', 'last_updated',
+            'diameter', 'diameter_unit', 'color', 'cooling_port', 'description', 'created', 'last_updated',
         ]
         brief_fields = ('id', 'url', 'display', 'name', 'description')
 

+ 0 - 21
netbox/dcim/choices.py

@@ -2219,27 +2219,6 @@ class CoolingConnectorTypeChoices(ChoiceSet):
     ]
 
 
-class CoolingDiameterChoices(ChoiceSet):
-
-    DN10 = 'dn10'
-    DN15 = 'dn15'
-    DN20 = 'dn20'
-    DN25 = 'dn25'
-    DN32 = 'dn32'
-    DN40 = 'dn40'
-    DN50 = 'dn50'
-
-    CHOICES = [
-        (DN10, 'DN10'),
-        (DN15, 'DN15'),
-        (DN20, 'DN20'),
-        (DN25, 'DN25'),
-        (DN32, 'DN32'),
-        (DN40, 'DN40'),
-        (DN50, 'DN50'),
-    ]
-
-
 #
 # VDC
 #

+ 7 - 16
netbox/dcim/filtersets.py

@@ -1015,8 +1015,8 @@ class CoolingPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceType
     class Meta:
         model = CoolingPortTemplate
         fields = (
-            'id', 'name', 'label', 'type', 'connector_type', 'diameter', 'maximum_flow', 'heat_capacity',
-            'description',
+            'id', 'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow',
+            'heat_capacity', 'description',
         )
 
 
@@ -1030,7 +1030,7 @@ class CoolingOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTy
 
     class Meta:
         model = CoolingOutletTemplate
-        fields = ('id', 'name', 'label', 'type', 'connector_type', 'diameter', 'color', 'description')
+        fields = ('id', 'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'color', 'description')
 
 
 @register_filterset
@@ -2078,17 +2078,12 @@ class CoolingPortFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilterSe
         distinct=False,
         null_value=None
     )
-    diameter = django_filters.MultipleChoiceFilter(
-        choices=CoolingDiameterChoices,
-        distinct=False,
-        null_value=None
-    )
 
     class Meta:
         model = CoolingPort
         fields = (
-            'id', 'name', 'label', 'maximum_flow', 'heat_capacity', 'description', 'mark_connected', 'cable_end',
-            'cable_connector',
+            'id', 'name', 'label', 'diameter', 'diameter_unit', 'maximum_flow', 'heat_capacity', 'description',
+            'mark_connected', 'cable_end', 'cable_connector',
         )
 
 
@@ -2104,11 +2099,6 @@ class CoolingOutletFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilter
         distinct=False,
         null_value=None
     )
-    diameter = django_filters.MultipleChoiceFilter(
-        choices=CoolingDiameterChoices,
-        distinct=False,
-        null_value=None
-    )
     cooling_port_id = django_filters.ModelMultipleChoiceFilter(
         queryset=CoolingPort.objects.all(),
         distinct=False,
@@ -2118,7 +2108,8 @@ class CoolingOutletFilterSet(ModularDeviceComponentFilterSet, CabledObjectFilter
     class Meta:
         model = CoolingOutlet
         fields = (
-            'id', 'name', 'label', 'description', 'color', 'mark_connected', 'cable_end', 'cable_connector',
+            'id', 'name', 'label', 'diameter', 'diameter_unit', 'description', 'color', 'mark_connected', 'cable_end',
+            'cable_connector',
         )
 
 

+ 46 - 14
netbox/dcim/forms/bulk_edit.py

@@ -1346,9 +1346,13 @@ class CoolingPortTemplateBulkEditForm(ComponentTemplateBulkEditForm):
         choices=add_blank_choice(CoolingConnectorTypeChoices),
         required=False
     )
-    diameter = forms.ChoiceField(
+    diameter = forms.DecimalField(
         label=_('Diameter'),
-        choices=add_blank_choice(CoolingDiameterChoices),
+        required=False
+    )
+    diameter_unit = forms.ChoiceField(
+        label=_('Diameter unit'),
+        choices=add_blank_choice(DiameterUnitChoices),
         required=False
     )
     maximum_flow = forms.DecimalField(
@@ -1366,8 +1370,16 @@ class CoolingPortTemplateBulkEditForm(ComponentTemplateBulkEditForm):
         required=False
     )
 
+    fieldsets = (
+        FieldSet(
+            'label', 'type', 'connector_type',
+            InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
+            'maximum_flow', 'heat_capacity', 'description',
+        ),
+    )
     nullable_fields = (
-        'label', 'type', 'connector_type', 'diameter', 'maximum_flow', 'heat_capacity', 'description',
+        'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow', 'heat_capacity',
+        'description',
     )
 
 
@@ -1398,9 +1410,13 @@ class CoolingOutletTemplateBulkEditForm(ComponentTemplateBulkEditForm):
         choices=add_blank_choice(CoolingConnectorTypeChoices),
         required=False
     )
-    diameter = forms.ChoiceField(
+    diameter = forms.DecimalField(
         label=_('Diameter'),
-        choices=add_blank_choice(CoolingDiameterChoices),
+        required=False
+    )
+    diameter_unit = forms.ChoiceField(
+        label=_('Diameter unit'),
+        choices=add_blank_choice(DiameterUnitChoices),
         required=False
     )
     color = ColorField(
@@ -1417,8 +1433,15 @@ class CoolingOutletTemplateBulkEditForm(ComponentTemplateBulkEditForm):
         required=False
     )
 
+    fieldsets = (
+        FieldSet(
+            'label', 'type', 'connector_type',
+            InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
+            'color', 'cooling_port', 'description',
+        ),
+    )
     nullable_fields = (
-        'label', 'type', 'connector_type', 'diameter', 'cooling_port', 'description',
+        'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'cooling_port', 'description',
     )
 
     def __init__(self, *args, **kwargs):
@@ -1734,8 +1757,8 @@ class PowerOutletBulkEditForm(
 class CoolingPortBulkEditForm(
     ComponentBulkEditForm,
     form_from_model(CoolingPort, [
-        'label', 'type', 'connector_type', 'diameter', 'maximum_flow', 'heat_capacity', 'mark_connected',
-        'description'
+        'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow', 'heat_capacity',
+        'mark_connected', 'description'
     ])
 ):
     mark_connected = forms.NullBooleanField(
@@ -1746,11 +1769,16 @@ class CoolingPortBulkEditForm(
 
     model = CoolingPort
     fieldsets = (
-        FieldSet('module', 'type', 'connector_type', 'diameter', 'label', 'description', 'mark_connected'),
+        FieldSet(
+            'module', 'type', 'connector_type',
+            InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
+            'label', 'description', 'mark_connected',
+        ),
         FieldSet('maximum_flow', 'heat_capacity', name=_('Characteristics')),
     )
     nullable_fields = (
-        'module', 'label', 'type', 'connector_type', 'diameter', 'maximum_flow', 'heat_capacity', 'description',
+        'module', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow', 'heat_capacity',
+        'description',
     )
 
 
@@ -1758,7 +1786,10 @@ class CoolingOutletBulkEditForm(
     ComponentBulkEditForm,
     form_from_model(
         CoolingOutlet,
-        ['label', 'type', 'connector_type', 'diameter', 'color', 'cooling_port', 'mark_connected', 'description']
+        [
+            'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'color', 'cooling_port', 'mark_connected',
+            'description'
+        ]
     )
 ):
     mark_connected = forms.NullBooleanField(
@@ -1770,12 +1801,13 @@ class CoolingOutletBulkEditForm(
     model = CoolingOutlet
     fieldsets = (
         FieldSet(
-            'module', 'type', 'connector_type', 'diameter', 'label', 'description', 'mark_connected', 'color',
-            'cooling_port'
+            'module', 'type', 'connector_type',
+            InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
+            'label', 'description', 'mark_connected', 'color', 'cooling_port',
         ),
     )
     nullable_fields = (
-        'module', 'label', 'type', 'connector_type', 'diameter', 'cooling_port', 'description',
+        'module', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'cooling_port', 'description',
     )
 
     def __init__(self, *args, **kwargs):

+ 12 - 12
netbox/dcim/forms/bulk_import.py

@@ -988,18 +988,18 @@ class CoolingPortImportForm(OwnerCSVMixin, NetBoxModelImportForm):
         required=False,
         help_text=_('Physical connector type')
     )
-    diameter = CSVChoiceField(
-        label=_('Diameter'),
-        choices=CoolingDiameterChoices,
+    diameter_unit = CSVChoiceField(
+        label=_('Diameter unit'),
+        choices=DiameterUnitChoices,
         required=False,
-        help_text=_('Nominal connector diameter')
+        help_text=_('Diameter unit')
     )
 
     class Meta:
         model = CoolingPort
         fields = (
-            'device', 'name', 'label', 'type', 'connector_type', 'diameter', 'maximum_flow', 'heat_capacity',
-            'mark_connected', 'description', 'owner', 'tags',
+            'device', 'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow',
+            'heat_capacity', 'mark_connected', 'description', 'owner', 'tags',
         )
 
 
@@ -1021,11 +1021,11 @@ class CoolingOutletImportForm(OwnerCSVMixin, NetBoxModelImportForm):
         required=False,
         help_text=_('Physical connector type')
     )
-    diameter = CSVChoiceField(
-        label=_('Diameter'),
-        choices=CoolingDiameterChoices,
+    diameter_unit = CSVChoiceField(
+        label=_('Diameter unit'),
+        choices=DiameterUnitChoices,
         required=False,
-        help_text=_('Nominal connector diameter')
+        help_text=_('Diameter unit')
     )
     cooling_port = CSVModelChoiceField(
         label=_('Cooling port'),
@@ -1038,8 +1038,8 @@ class CoolingOutletImportForm(OwnerCSVMixin, NetBoxModelImportForm):
     class Meta:
         model = CoolingOutlet
         fields = (
-            'device', 'name', 'label', 'type', 'connector_type', 'diameter', 'color', 'mark_connected', 'cooling_port',
-            'description', 'owner', 'tags',
+            'device', 'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'color', 'mark_connected',
+            'cooling_port', 'description', 'owner', 'tags',
         )
 
     def __init__(self, *args, **kwargs):

+ 30 - 12
netbox/dcim/forms/filtersets.py

@@ -1782,7 +1782,7 @@ class CoolingPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
     model = CoolingPort
     fieldsets = (
         FieldSet('q', 'filter_id', 'tag'),
-        FieldSet('name', 'label', 'type', 'connector_type', 'diameter', name=_('Attributes')),
+        FieldSet('name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', name=_('Attributes')),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
         FieldSet(
             'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
@@ -1801,9 +1801,13 @@ class CoolingPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
         choices=CoolingConnectorTypeChoices,
         required=False
     )
-    diameter = forms.MultipleChoiceField(
+    diameter = forms.DecimalField(
         label=_('Diameter'),
-        choices=CoolingDiameterChoices,
+        required=False
+    )
+    diameter_unit = forms.ChoiceField(
+        label=_('Diameter unit'),
+        choices=add_blank_choice(DiameterUnitChoices),
         required=False
     )
     tag = TagFilterField(model)
@@ -1813,7 +1817,7 @@ class CoolingPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
     model = CoolingPortTemplate
     fieldsets = (
         FieldSet('q', 'filter_id', 'tag'),
-        FieldSet('name', 'label', 'type', 'connector_type', 'diameter', name=_('Attributes')),
+        FieldSet('name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', name=_('Attributes')),
         FieldSet('device_type_id', 'module_type_id', name=_('Device')),
     )
     type = forms.MultipleChoiceField(
@@ -1826,9 +1830,13 @@ class CoolingPortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
         choices=CoolingConnectorTypeChoices,
         required=False
     )
-    diameter = forms.MultipleChoiceField(
+    diameter = forms.DecimalField(
         label=_('Diameter'),
-        choices=CoolingDiameterChoices,
+        required=False
+    )
+    diameter_unit = forms.ChoiceField(
+        label=_('Diameter unit'),
+        choices=add_blank_choice(DiameterUnitChoices),
         required=False
     )
 
@@ -1837,7 +1845,9 @@ class CoolingOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm)
     model = CoolingOutlet
     fieldsets = (
         FieldSet('q', 'filter_id', 'tag'),
-        FieldSet('name', 'label', 'type', 'connector_type', 'diameter', 'color', name=_('Attributes')),
+        FieldSet(
+            'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'color', name=_('Attributes')
+        ),
         FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
         FieldSet(
             'tenant_id', 'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
@@ -1856,9 +1866,13 @@ class CoolingOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm)
         choices=CoolingConnectorTypeChoices,
         required=False
     )
-    diameter = forms.MultipleChoiceField(
+    diameter = forms.DecimalField(
         label=_('Diameter'),
-        choices=CoolingDiameterChoices,
+        required=False
+    )
+    diameter_unit = forms.ChoiceField(
+        label=_('Diameter unit'),
+        choices=add_blank_choice(DiameterUnitChoices),
         required=False
     )
     color = ColorField(
@@ -1872,7 +1886,7 @@ class CoolingOutletTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
     model = CoolingOutletTemplate
     fieldsets = (
         FieldSet('q', 'filter_id', 'tag'),
-        FieldSet('name', 'label', 'type', 'connector_type', 'diameter', name=_('Attributes')),
+        FieldSet('name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', name=_('Attributes')),
         FieldSet('device_type_id', 'module_type_id', name=_('Device')),
     )
     type = forms.MultipleChoiceField(
@@ -1885,9 +1899,13 @@ class CoolingOutletTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
         choices=CoolingConnectorTypeChoices,
         required=False
     )
-    diameter = forms.MultipleChoiceField(
+    diameter = forms.DecimalField(
         label=_('Diameter'),
-        choices=CoolingDiameterChoices,
+        required=False
+    )
+    diameter_unit = forms.ChoiceField(
+        label=_('Diameter unit'),
+        choices=add_blank_choice(DiameterUnitChoices),
         required=False
     )
 

+ 20 - 14
netbox/dcim/forms/model_forms.py

@@ -1251,15 +1251,17 @@ class CoolingPortTemplateForm(ModularComponentTemplateForm):
                 FieldSet('device_type', name=_('Device Type')),
                 FieldSet('module_type', name=_('Module Type')),
             ),
-            'name', 'label', 'type', 'connector_type', 'diameter', 'maximum_flow', 'heat_capacity', 'description',
+            'name', 'label', 'type', 'connector_type',
+            InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
+            'maximum_flow', 'heat_capacity', 'description',
         ),
     )
 
     class Meta:
         model = CoolingPortTemplate
         fields = [
-            'device_type', 'module_type', 'name', 'label', 'type', 'connector_type', 'diameter', 'maximum_flow',
-            'heat_capacity', 'description',
+            'device_type', 'module_type', 'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit',
+            'maximum_flow', 'heat_capacity', 'description',
         ]
 
 
@@ -1279,15 +1281,17 @@ class CoolingOutletTemplateForm(ModularComponentTemplateForm):
                 FieldSet('device_type', name=_('Device Type')),
                 FieldSet('module_type', name=_('Module Type')),
             ),
-            'name', 'label', 'type', 'connector_type', 'diameter', 'color', 'cooling_port', 'description',
+            'name', 'label', 'type', 'connector_type',
+            InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
+            'color', 'cooling_port', 'description',
         ),
     )
 
     class Meta:
         model = CoolingOutletTemplate
         fields = [
-            'device_type', 'module_type', 'name', 'label', 'type', 'connector_type', 'diameter', 'color',
-            'cooling_port', 'description',
+            'device_type', 'module_type', 'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit',
+            'color', 'cooling_port', 'description',
         ]
 
 
@@ -1657,16 +1661,17 @@ class PowerOutletForm(ModularDeviceComponentForm):
 class CoolingPortForm(ModularDeviceComponentForm):
     fieldsets = (
         FieldSet(
-            'device', 'module', 'name', 'label', 'type', 'connector_type', 'diameter', 'maximum_flow',
-            'heat_capacity', 'mark_connected', 'description', 'tags',
+            'device', 'module', 'name', 'label', 'type', 'connector_type',
+            InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
+            'maximum_flow', 'heat_capacity', 'mark_connected', 'description', 'tags',
         ),
     )
 
     class Meta:
         model = CoolingPort
         fields = [
-            'device', 'module', 'name', 'label', 'type', 'connector_type', 'diameter', 'maximum_flow', 'heat_capacity',
-            'mark_connected', 'description', 'owner', 'tags',
+            'device', 'module', 'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow',
+            'heat_capacity', 'mark_connected', 'description', 'owner', 'tags',
         ]
 
 
@@ -1682,16 +1687,17 @@ class CoolingOutletForm(ModularDeviceComponentForm):
 
     fieldsets = (
         FieldSet(
-            'device', 'module', 'name', 'label', 'type', 'connector_type', 'diameter', 'color', 'cooling_port',
-            'mark_connected', 'description', 'tags',
+            'device', 'module', 'name', 'label', 'type', 'connector_type',
+            InlineFields('diameter', 'diameter_unit', label=_('Diameter')),
+            'color', 'cooling_port', 'mark_connected', 'description', 'tags',
         ),
     )
 
     class Meta:
         model = CoolingOutlet
         fields = [
-            'device', 'module', 'name', 'label', 'type', 'connector_type', 'diameter', 'color', 'cooling_port',
-            'mark_connected', 'description', 'tags',
+            'device', 'module', 'name', 'label', 'type', 'connector_type', 'diameter', 'diameter_unit', 'color',
+            'cooling_port', 'mark_connected', 'description', 'tags',
         ]
 
 

+ 3 - 2
netbox/dcim/graphql/enums.py

@@ -1,6 +1,7 @@
 import strawberry
 
 from dcim.choices import *
+from netbox.choices import DiameterUnitChoices
 
 __all__ = (
     'CableEndEnum',
@@ -9,7 +10,6 @@ __all__ = (
     'ConsolePortSpeedEnum',
     'ConsolePortTypeEnum',
     'CoolingConnectorTypeEnum',
-    'CoolingDiameterEnum',
     'CoolingFeedStatusEnum',
     'CoolingFeedTypeEnum',
     'CoolingMethodEnum',
@@ -18,6 +18,7 @@ __all__ = (
     'DeviceAirflowEnum',
     'DeviceFaceEnum',
     'DeviceStatusEnum',
+    'DiameterUnitEnum',
     'FluidTypeEnum',
     'InterfaceDuplexEnum',
     'InterfaceKindEnum',
@@ -56,7 +57,6 @@ CableTypeEnum = strawberry.enum(CableTypeChoices.as_enum(prefix='type'))
 ConsolePortSpeedEnum = strawberry.enum(ConsolePortSpeedChoices.as_enum(prefix='speed'))
 ConsolePortTypeEnum = strawberry.enum(ConsolePortTypeChoices.as_enum(prefix='type'))
 CoolingConnectorTypeEnum = strawberry.enum(CoolingConnectorTypeChoices.as_enum(prefix='type'))
-CoolingDiameterEnum = strawberry.enum(CoolingDiameterChoices.as_enum(prefix='diameter'))
 CoolingFeedStatusEnum = strawberry.enum(CoolingFeedStatusChoices.as_enum(prefix='status'))
 CoolingFeedTypeEnum = strawberry.enum(CoolingFeedTypeChoices.as_enum(prefix='type'))
 CoolingMethodEnum = strawberry.enum(CoolingMethodChoices.as_enum(prefix='method'))
@@ -65,6 +65,7 @@ CoolingSourceTypeEnum = strawberry.enum(CoolingSourceTypeChoices.as_enum(prefix=
 DeviceAirflowEnum = strawberry.enum(DeviceAirflowChoices.as_enum(prefix='airflow'))
 DeviceFaceEnum = strawberry.enum(DeviceFaceChoices.as_enum(prefix='face'))
 DeviceStatusEnum = strawberry.enum(DeviceStatusChoices.as_enum(prefix='status'))
+DiameterUnitEnum = strawberry.enum(DiameterUnitChoices.as_enum(prefix='unit'))
 FluidTypeEnum = strawberry.enum(FluidTypeChoices.as_enum(prefix='fluid'))
 InterfaceDuplexEnum = strawberry.enum(InterfaceDuplexChoices.as_enum(prefix='duplex'))
 InterfaceKindEnum = strawberry.enum(InterfaceKindChoices.as_enum(prefix='kind'))

+ 16 - 4
netbox/dcim/graphql/filters.py

@@ -980,7 +980,10 @@ class CoolingOutletFilter(ModularComponentFilterMixin, CabledObjectModelFilterMi
     connector_type: BaseFilterLookup[
         Annotated['CoolingConnectorTypeEnum', strawberry.lazy('dcim.graphql.enums')]
     ] | None = strawberry_django.filter_field()
-    diameter: BaseFilterLookup[Annotated['CoolingDiameterEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
+    diameter: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
+        strawberry_django.filter_field()
+    )
+    diameter_unit: BaseFilterLookup[Annotated['DiameterUnitEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
         strawberry_django.filter_field()
     )
     cooling_port: Annotated['CoolingPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
@@ -1000,7 +1003,10 @@ class CoolingOutletTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLog
     connector_type: BaseFilterLookup[
         Annotated['CoolingConnectorTypeEnum', strawberry.lazy('dcim.graphql.enums')]
     ] | None = strawberry_django.filter_field()
-    diameter: BaseFilterLookup[Annotated['CoolingDiameterEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
+    diameter: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
+        strawberry_django.filter_field()
+    )
+    diameter_unit: BaseFilterLookup[Annotated['DiameterUnitEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
         strawberry_django.filter_field()
     )
     cooling_port: Annotated['CoolingPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
@@ -1017,7 +1023,10 @@ class CoolingPortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixi
     connector_type: BaseFilterLookup[
         Annotated['CoolingConnectorTypeEnum', strawberry.lazy('dcim.graphql.enums')]
     ] | None = strawberry_django.filter_field()
-    diameter: BaseFilterLookup[Annotated['CoolingDiameterEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
+    diameter: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
+        strawberry_django.filter_field()
+    )
+    diameter_unit: BaseFilterLookup[Annotated['DiameterUnitEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
         strawberry_django.filter_field()
     )
     maximum_flow: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
@@ -1036,7 +1045,10 @@ class CoolingPortTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLogge
     connector_type: BaseFilterLookup[
         Annotated['CoolingConnectorTypeEnum', strawberry.lazy('dcim.graphql.enums')]
     ] | None = strawberry_django.filter_field()
-    diameter: BaseFilterLookup[Annotated['CoolingDiameterEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
+    diameter: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
+        strawberry_django.filter_field()
+    )
+    diameter_unit: BaseFilterLookup[Annotated['DiameterUnitEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
         strawberry_django.filter_field()
     )
     maximum_flow: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (

+ 65 - 5
netbox/dcim/migrations/0241_device_cooling_method_device_cooling_outlet_count_and_more.py

@@ -1,4 +1,4 @@
-# Generated by Django 6.0.6 on 2026-06-23 18:45
+# Generated by Django 6.0.6 on 2026-06-23 20:57
 
 import django.contrib.postgres.fields
 import django.core.validators
@@ -140,6 +140,22 @@ class Migration(migrations.Migration):
                         encoder=utilities.json.CustomFieldJSONEncoder,
                     ),
                 ),
+                (
+                    "diameter",
+                    models.DecimalField(
+                        blank=True, decimal_places=2, max_digits=8, null=True
+                    ),
+                ),
+                (
+                    "diameter_unit",
+                    models.CharField(blank=True, max_length=50, null=True),
+                ),
+                (
+                    "_abs_diameter",
+                    models.DecimalField(
+                        blank=True, decimal_places=4, max_digits=13, null=True
+                    ),
+                ),
                 ("name", models.CharField(db_collation="natural_sort", max_length=64)),
                 ("label", models.CharField(blank=True, max_length=64)),
                 ("description", models.CharField(blank=True, max_length=200)),
@@ -174,7 +190,6 @@ class Migration(migrations.Migration):
                     "connector_type",
                     models.CharField(blank=True, max_length=50, null=True),
                 ),
-                ("diameter", models.CharField(blank=True, max_length=50, null=True)),
                 (
                     "maximum_flow",
                     models.DecimalField(
@@ -310,6 +325,22 @@ class Migration(migrations.Migration):
                         encoder=utilities.json.CustomFieldJSONEncoder,
                     ),
                 ),
+                (
+                    "diameter",
+                    models.DecimalField(
+                        blank=True, decimal_places=2, max_digits=8, null=True
+                    ),
+                ),
+                (
+                    "diameter_unit",
+                    models.CharField(blank=True, max_length=50, null=True),
+                ),
+                (
+                    "_abs_diameter",
+                    models.DecimalField(
+                        blank=True, decimal_places=4, max_digits=13, null=True
+                    ),
+                ),
                 ("name", models.CharField(db_collation="natural_sort", max_length=64)),
                 ("label", models.CharField(blank=True, max_length=64)),
                 ("description", models.CharField(blank=True, max_length=200)),
@@ -344,7 +375,6 @@ class Migration(migrations.Migration):
                     "connector_type",
                     models.CharField(blank=True, max_length=50, null=True),
                 ),
-                ("diameter", models.CharField(blank=True, max_length=50, null=True)),
                 ("color", utilities.fields.ColorField(blank=True, max_length=6)),
                 (
                     "_location",
@@ -463,6 +493,22 @@ class Migration(migrations.Migration):
                 ),
                 ("created", models.DateTimeField(auto_now_add=True, null=True)),
                 ("last_updated", models.DateTimeField(auto_now=True, null=True)),
+                (
+                    "diameter",
+                    models.DecimalField(
+                        blank=True, decimal_places=2, max_digits=8, null=True
+                    ),
+                ),
+                (
+                    "diameter_unit",
+                    models.CharField(blank=True, max_length=50, null=True),
+                ),
+                (
+                    "_abs_diameter",
+                    models.DecimalField(
+                        blank=True, decimal_places=4, max_digits=13, null=True
+                    ),
+                ),
                 ("name", models.CharField(db_collation="natural_sort", max_length=64)),
                 ("label", models.CharField(blank=True, max_length=64)),
                 ("description", models.CharField(blank=True, max_length=200)),
@@ -471,7 +517,6 @@ class Migration(migrations.Migration):
                     "connector_type",
                     models.CharField(blank=True, max_length=50, null=True),
                 ),
-                ("diameter", models.CharField(blank=True, max_length=50, null=True)),
                 (
                     "maximum_flow",
                     models.DecimalField(
@@ -536,6 +581,22 @@ class Migration(migrations.Migration):
                 ),
                 ("created", models.DateTimeField(auto_now_add=True, null=True)),
                 ("last_updated", models.DateTimeField(auto_now=True, null=True)),
+                (
+                    "diameter",
+                    models.DecimalField(
+                        blank=True, decimal_places=2, max_digits=8, null=True
+                    ),
+                ),
+                (
+                    "diameter_unit",
+                    models.CharField(blank=True, max_length=50, null=True),
+                ),
+                (
+                    "_abs_diameter",
+                    models.DecimalField(
+                        blank=True, decimal_places=4, max_digits=13, null=True
+                    ),
+                ),
                 ("name", models.CharField(db_collation="natural_sort", max_length=64)),
                 ("label", models.CharField(blank=True, max_length=64)),
                 ("description", models.CharField(blank=True, max_length=200)),
@@ -544,7 +605,6 @@ class Migration(migrations.Migration):
                     "connector_type",
                     models.CharField(blank=True, max_length=50, null=True),
                 ),
-                ("diameter", models.CharField(blank=True, max_length=50, null=True)),
                 ("color", utilities.fields.ColorField(blank=True, max_length=6)),
                 (
                     "device_type",

+ 11 - 18
netbox/dcim/models/device_component_templates.py

@@ -12,6 +12,7 @@ from dcim.models.mixins import InterfaceValidationMixin
 from dcim.utils import get_module_bay_positions, resolve_module_placeholder
 from netbox.models import ChangeLoggedModel
 from netbox.models.ltree import LtreeManager, LtreeModel
+from netbox.models.mixins import DiameterMixin
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.ordering import naturalize_interface
 from utilities.tracking import TrackingModelMixin
@@ -436,7 +437,7 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
         }
 
 
-class CoolingPortTemplate(ModularComponentTemplateModel):
+class CoolingPortTemplate(DiameterMixin, ModularComponentTemplateModel):
     """
     A template for a CoolingPort to be created for a new Device.
     """
@@ -454,13 +455,7 @@ class CoolingPortTemplate(ModularComponentTemplateModel):
         blank=True,
         null=True
     )
-    diameter = models.CharField(
-        verbose_name=_('diameter'),
-        max_length=50,
-        choices=CoolingDiameterChoices,
-        blank=True,
-        null=True
-    )
+    # diameter, diameter_unit, _abs_diameter provided by DiameterMixin
     maximum_flow = models.DecimalField(
         verbose_name=_('maximum flow'),
         max_digits=8,
@@ -493,6 +488,7 @@ class CoolingPortTemplate(ModularComponentTemplateModel):
             type=self.type,
             connector_type=self.connector_type,
             diameter=self.diameter,
+            diameter_unit=self.diameter_unit,
             maximum_flow=self.maximum_flow,
             heat_capacity=self.heat_capacity,
             **kwargs
@@ -504,7 +500,8 @@ class CoolingPortTemplate(ModularComponentTemplateModel):
             'name': self.name,
             'type': self.type,
             'connector_type': self.connector_type,
-            'diameter': self.diameter,
+            'diameter': float(self.diameter) if self.diameter is not None else None,
+            'diameter_unit': self.diameter_unit,
             'maximum_flow': float(self.maximum_flow) if self.maximum_flow is not None else None,
             'heat_capacity': float(self.heat_capacity) if self.heat_capacity is not None else None,
             'label': self.label,
@@ -512,7 +509,7 @@ class CoolingPortTemplate(ModularComponentTemplateModel):
         }
 
 
-class CoolingOutletTemplate(ModularComponentTemplateModel):
+class CoolingOutletTemplate(DiameterMixin, ModularComponentTemplateModel):
     """
     A template for a CoolingOutlet to be created for a new Device.
     """
@@ -530,13 +527,7 @@ class CoolingOutletTemplate(ModularComponentTemplateModel):
         blank=True,
         null=True
     )
-    diameter = models.CharField(
-        verbose_name=_('diameter'),
-        max_length=50,
-        choices=CoolingDiameterChoices,
-        blank=True,
-        null=True
-    )
+    # diameter, diameter_unit, _abs_diameter provided by DiameterMixin
     color = ColorField(
         verbose_name=_('color'),
         blank=True
@@ -585,6 +576,7 @@ class CoolingOutletTemplate(ModularComponentTemplateModel):
             type=self.type,
             connector_type=self.connector_type,
             diameter=self.diameter,
+            diameter_unit=self.diameter_unit,
             color=self.color,
             cooling_port=cooling_port,
             **kwargs
@@ -596,7 +588,8 @@ class CoolingOutletTemplate(ModularComponentTemplateModel):
             'name': self.name,
             'type': self.type,
             'connector_type': self.connector_type,
-            'diameter': self.diameter,
+            'diameter': float(self.diameter) if self.diameter is not None else None,
+            'diameter_unit': self.diameter_unit,
             'color': self.color,
             'cooling_port': self.cooling_port.name if self.cooling_port else None,
             'label': self.label,

+ 9 - 21
netbox/dcim/models/device_components.py

@@ -16,7 +16,7 @@ from dcim.models.mixins import InterfaceValidationMixin
 from netbox.choices import ColorChoices
 from netbox.models import NetBoxModel, OrganizationalModel
 from netbox.models.ltree import LtreeManager, LtreeModel, SortPathField
-from netbox.models.mixins import OwnerMixin
+from netbox.models.mixins import DiameterMixin, OwnerMixin
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.ordering import naturalize_interface
 from utilities.query_functions import CollateAsChar
@@ -689,7 +689,7 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
 # Cooling components
 #
 
-class CoolingPort(ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin):
+class CoolingPort(DiameterMixin, ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin):
     """
     A coolant intake/outlet port within a Device (e.g. a server cold-plate inlet or CDU intake).
     CoolingPorts connect to CoolingOutlets.
@@ -710,14 +710,7 @@ class CoolingPort(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
         null=True,
         help_text=_('Physical connector type')
     )
-    diameter = models.CharField(
-        verbose_name=_('diameter'),
-        max_length=50,
-        choices=CoolingDiameterChoices,
-        blank=True,
-        null=True,
-        help_text=_('Nominal connector diameter')
-    )
+    # diameter, diameter_unit, _abs_diameter provided by DiameterMixin
     maximum_flow = models.DecimalField(
         verbose_name=_('maximum flow'),
         max_digits=8,
@@ -737,7 +730,9 @@ class CoolingPort(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
         help_text=_('Heat removal capacity (kW)')
     )
 
-    clone_fields = ('device', 'module', 'type', 'connector_type', 'diameter', 'maximum_flow', 'heat_capacity')
+    clone_fields = (
+        'device', 'module', 'type', 'connector_type', 'diameter', 'diameter_unit', 'maximum_flow', 'heat_capacity',
+    )
 
     class Meta(ModularComponentModel.Meta):
         verbose_name = _('cooling port')
@@ -747,7 +742,7 @@ class CoolingPort(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
         return CoolingFeedTypeChoices.colors.get(self.type)
 
 
-class CoolingOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin):
+class CoolingOutlet(DiameterMixin, ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin):
     """
     A coolant outlet within a Device (e.g. a CDU or manifold outlet) which feeds a CoolingPort.
     """
@@ -767,14 +762,7 @@ class CoolingOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Trac
         null=True,
         help_text=_('Physical connector type')
     )
-    diameter = models.CharField(
-        verbose_name=_('diameter'),
-        max_length=50,
-        choices=CoolingDiameterChoices,
-        blank=True,
-        null=True,
-        help_text=_('Nominal connector diameter')
-    )
+    # diameter, diameter_unit, _abs_diameter provided by DiameterMixin
     cooling_port = models.ForeignKey(
         to='dcim.CoolingPort',
         on_delete=models.SET_NULL,
@@ -787,7 +775,7 @@ class CoolingOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, Trac
         blank=True
     )
 
-    clone_fields = ('device', 'module', 'type', 'connector_type', 'diameter', 'cooling_port')
+    clone_fields = ('device', 'module', 'type', 'connector_type', 'diameter', 'diameter_unit', 'cooling_port')
 
     class Meta(ModularComponentModel.Meta):
         verbose_name = _('cooling outlet')

+ 22 - 3
netbox/dcim/tables/cooling.py

@@ -9,7 +9,12 @@ from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
 
 from .devices import CableTerminationTable, DeviceComponentTable, ModularDeviceComponentTable, PathEndpointTable
 from .devicetypes import ComponentTemplateTable
-from .template_code import COOLINGOUTLET_BUTTONS, COOLINGPORT_BUTTONS, MODULAR_COMPONENT_TEMPLATE_BUTTONS
+from .template_code import (
+    COOLINGOUTLET_BUTTONS,
+    COOLINGPORT_BUTTONS,
+    DIAMETER,
+    MODULAR_COMPONENT_TEMPLATE_BUTTONS,
+)
 
 __all__ = (
     'CoolingFeedTable',
@@ -164,8 +169,10 @@ class CoolingPortTable(ModularDeviceComponentTable, PathEndpointTable):
     connector_type = columns.ChoiceFieldColumn(
         verbose_name=_('Connector Type'),
     )
-    diameter = columns.ChoiceFieldColumn(
+    diameter = columns.TemplateColumn(
         verbose_name=_('Diameter'),
+        template_code=DIAMETER,
+        order_by=('_abs_diameter', 'diameter_unit')
     )
     maximum_flow = tables.Column(
         verbose_name=_('Maximum flow (L/min)')
@@ -208,8 +215,10 @@ class CoolingOutletTable(ModularDeviceComponentTable, PathEndpointTable):
     connector_type = columns.ChoiceFieldColumn(
         verbose_name=_('Connector Type'),
     )
-    diameter = columns.ChoiceFieldColumn(
+    diameter = columns.TemplateColumn(
         verbose_name=_('Diameter'),
+        template_code=DIAMETER,
+        order_by=('_abs_diameter', 'diameter_unit')
     )
     cooling_port = tables.Column(
         verbose_name=_('Cooling Port'),
@@ -238,6 +247,11 @@ class CoolingOutletTable(ModularDeviceComponentTable, PathEndpointTable):
 #
 
 class CoolingPortTemplateTable(ComponentTemplateTable):
+    diameter = columns.TemplateColumn(
+        verbose_name=_('Diameter'),
+        template_code=DIAMETER,
+        order_by=('_abs_diameter', 'diameter_unit')
+    )
     actions = columns.ActionsColumn(
         actions=('edit', 'delete'),
         extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
@@ -257,6 +271,11 @@ class CoolingPortTemplateTable(ComponentTemplateTable):
 #
 
 class CoolingOutletTemplateTable(ComponentTemplateTable):
+    diameter = columns.TemplateColumn(
+        verbose_name=_('Diameter'),
+        template_code=DIAMETER,
+        order_by=('_abs_diameter', 'diameter_unit')
+    )
     color = columns.ColorColumn(
         verbose_name=_('Color'),
     )

+ 4 - 0
netbox/dcim/tables/template_code.py

@@ -52,6 +52,10 @@ WEIGHT = """
 {% display_weight record.weight record.weight_unit record.abs_weight %}
 """
 
+DIAMETER = """
+{% if record.diameter %}{{ record.diameter }} {{ record.get_diameter_unit_display }}{% endif %}
+"""
+
 DEVICE_LINK = """
 {{ record.label|default:'<span class="badge text-bg-info">Unnamed device</span>' }}
 """

+ 31 - 17
netbox/dcim/tests/test_filtersets.py

@@ -1,3 +1,5 @@
+from decimal import Decimal
+
 from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.test import TestCase
@@ -8,7 +10,7 @@ from dcim.filtersets import *
 from dcim.models import *
 from ipam.choices import VLANQinQRoleChoices
 from ipam.models import ASN, RIR, VLAN, VRF, IPAddress, VLANTranslationPolicy
-from netbox.choices import ColorChoices, WeightUnitChoices
+from netbox.choices import ColorChoices, DiameterUnitChoices, WeightUnitChoices
 from tenancy.models import Tenant, TenantGroup
 from users.models import User
 from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
@@ -2048,7 +2050,8 @@ class CoolingPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTest
                 name='Cooling Port 1',
                 type=CoolingFeedTypeChoices.TYPE_SUPPLY,
                 connector_type=CoolingConnectorTypeChoices.TYPE_UQD,
-                diameter=CoolingDiameterChoices.DN25,
+                diameter=Decimal('25'),
+                diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 maximum_flow=100,
                 heat_capacity=50,
                 description='foobar1'
@@ -2058,7 +2061,8 @@ class CoolingPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTest
                 name='Cooling Port 2',
                 type=CoolingFeedTypeChoices.TYPE_RETURN,
                 connector_type=CoolingConnectorTypeChoices.TYPE_QDC,
-                diameter=CoolingDiameterChoices.DN32,
+                diameter=Decimal('32'),
+                diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 maximum_flow=200,
                 heat_capacity=100,
                 description='foobar2'
@@ -2068,7 +2072,8 @@ class CoolingPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTest
                 name='Cooling Port 3',
                 type=CoolingFeedTypeChoices.TYPE_SUPPLY,
                 connector_type=CoolingConnectorTypeChoices.TYPE_UQDB,
-                diameter=CoolingDiameterChoices.DN40,
+                diameter=Decimal('40'),
+                diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 maximum_flow=300,
                 heat_capacity=150,
                 description='foobar3'
@@ -2088,7 +2093,7 @@ class CoolingPortTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTest
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
     def test_diameter(self):
-        params = {'diameter': CoolingDiameterChoices.DN25}
+        params = {'diameter': [Decimal('25')]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
     def test_maximum_flow(self):
@@ -2122,7 +2127,8 @@ class CoolingOutletTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTe
                 name='Cooling Outlet 1',
                 type=CoolingFeedTypeChoices.TYPE_SUPPLY,
                 connector_type=CoolingConnectorTypeChoices.TYPE_UQD,
-                diameter=CoolingDiameterChoices.DN25,
+                diameter=Decimal('25'),
+                diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 color=ColorChoices.COLOR_RED,
                 description='foobar1'
             ),
@@ -2131,7 +2137,8 @@ class CoolingOutletTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTe
                 name='Cooling Outlet 2',
                 type=CoolingFeedTypeChoices.TYPE_RETURN,
                 connector_type=CoolingConnectorTypeChoices.TYPE_QDC,
-                diameter=CoolingDiameterChoices.DN32,
+                diameter=Decimal('32'),
+                diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 color=ColorChoices.COLOR_GREEN,
                 description='foobar2'
             ),
@@ -2140,7 +2147,8 @@ class CoolingOutletTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTe
                 name='Cooling Outlet 3',
                 type=CoolingFeedTypeChoices.TYPE_SUPPLY,
                 connector_type=CoolingConnectorTypeChoices.TYPE_UQDB,
-                diameter=CoolingDiameterChoices.DN40,
+                diameter=Decimal('40'),
+                diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 color=ColorChoices.COLOR_BLUE,
                 description='foobar3'
             ),
@@ -2159,7 +2167,7 @@ class CoolingOutletTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTe
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
     def test_diameter(self):
-        params = {'diameter': CoolingDiameterChoices.DN25}
+        params = {'diameter': [Decimal('25')]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
 
     def test_color(self):
@@ -4792,7 +4800,8 @@ class CoolingPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
                 label='A',
                 type=CoolingFeedTypeChoices.TYPE_SUPPLY,
                 connector_type=CoolingConnectorTypeChoices.TYPE_UQD,
-                diameter=CoolingDiameterChoices.DN25,
+                diameter=Decimal('25'),
+                diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 maximum_flow=100,
                 heat_capacity=50,
                 description='First',
@@ -4807,7 +4816,8 @@ class CoolingPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
                 label='B',
                 type=CoolingFeedTypeChoices.TYPE_RETURN,
                 connector_type=CoolingConnectorTypeChoices.TYPE_QDC,
-                diameter=CoolingDiameterChoices.DN32,
+                diameter=Decimal('32'),
+                diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 maximum_flow=200,
                 heat_capacity=100,
                 description='Second',
@@ -4822,7 +4832,8 @@ class CoolingPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
                 label='C',
                 type=CoolingFeedTypeChoices.TYPE_SUPPLY,
                 connector_type=CoolingConnectorTypeChoices.TYPE_UQDB,
-                diameter=CoolingDiameterChoices.DN40,
+                diameter=Decimal('40'),
+                diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 maximum_flow=300,
                 heat_capacity=150,
                 description='Third',
@@ -4859,7 +4870,7 @@ class CoolingPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
     def test_diameter(self):
-        params = {'diameter': [CoolingDiameterChoices.DN25, CoolingDiameterChoices.DN32]}
+        params = {'diameter': [Decimal('25'), Decimal('32')]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
     def test_maximum_flow(self):
@@ -5078,7 +5089,8 @@ class CoolingOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLogge
                 label='A',
                 type=CoolingFeedTypeChoices.TYPE_SUPPLY,
                 connector_type=CoolingConnectorTypeChoices.TYPE_UQD,
-                diameter=CoolingDiameterChoices.DN25,
+                diameter=Decimal('25'),
+                diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 description='First',
                 color='ff0000',
                 _site=devices[0].site,
@@ -5092,7 +5104,8 @@ class CoolingOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLogge
                 label='B',
                 type=CoolingFeedTypeChoices.TYPE_RETURN,
                 connector_type=CoolingConnectorTypeChoices.TYPE_QDC,
-                diameter=CoolingDiameterChoices.DN32,
+                diameter=Decimal('32'),
+                diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 description='Second',
                 color='00ff00',
                 _site=devices[1].site,
@@ -5106,7 +5119,8 @@ class CoolingOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLogge
                 label='C',
                 type=CoolingFeedTypeChoices.TYPE_SUPPLY,
                 connector_type=CoolingConnectorTypeChoices.TYPE_UQDB,
-                diameter=CoolingDiameterChoices.DN40,
+                diameter=Decimal('40'),
+                diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
                 description='Third',
                 color='0000ff',
                 _site=devices[2].site,
@@ -5146,7 +5160,7 @@ class CoolingOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLogge
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
     def test_diameter(self):
-        params = {'diameter': [CoolingDiameterChoices.DN25, CoolingDiameterChoices.DN32]}
+        params = {'diameter': [Decimal('25'), Decimal('32')]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
     def test_region(self):

+ 11 - 5
netbox/dcim/tests/test_models.py

@@ -1,3 +1,5 @@
+from decimal import Decimal
+
 from django.core.exceptions import ValidationError
 from django.db.models.signals import post_save
 from django.test import TestCase, tag
@@ -9,7 +11,7 @@ from dcim.models import *
 from extras.events import serialize_for_event
 from extras.models import CustomField
 from ipam.models import Prefix
-from netbox.choices import WeightUnitChoices
+from netbox.choices import DiameterUnitChoices, WeightUnitChoices
 from tenancy.models import Tenant
 from utilities.data import drange
 from virtualization.models import Cluster, ClusterType
@@ -2859,7 +2861,8 @@ class CoolingComponentTestCase(TestCase):
             name='Cooling Port 1',
             type=CoolingFeedTypeChoices.TYPE_SUPPLY,
             connector_type=CoolingConnectorTypeChoices.TYPE_UQD,
-            diameter=CoolingDiameterChoices.DN25,
+            diameter=Decimal('25'),
+            diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
             maximum_flow=100,
             heat_capacity=50
         )
@@ -2868,7 +2871,8 @@ class CoolingComponentTestCase(TestCase):
             name='Cooling Outlet 1',
             type=CoolingFeedTypeChoices.TYPE_SUPPLY,
             connector_type=CoolingConnectorTypeChoices.TYPE_UQD,
-            diameter=CoolingDiameterChoices.DN25
+            diameter=Decimal('25'),
+            diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER
         )
 
         device = Device.objects.create(
@@ -2883,7 +2887,8 @@ class CoolingComponentTestCase(TestCase):
             name='Cooling Port 1',
             type=CoolingFeedTypeChoices.TYPE_SUPPLY,
             connector_type=CoolingConnectorTypeChoices.TYPE_UQD,
-            diameter=CoolingDiameterChoices.DN25,
+            diameter=Decimal('25'),
+            diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER,
             maximum_flow=100,
             heat_capacity=50
         )
@@ -2894,7 +2899,8 @@ class CoolingComponentTestCase(TestCase):
             name='Cooling Outlet 1',
             type=CoolingFeedTypeChoices.TYPE_SUPPLY,
             connector_type=CoolingConnectorTypeChoices.TYPE_UQD,
-            diameter=CoolingDiameterChoices.DN25
+            diameter=Decimal('25'),
+            diameter_unit=DiameterUnitChoices.UNIT_MILLIMETER
         )
 
     def test_cooling_outlet_clean_different_device(self):

+ 23 - 12
netbox/dcim/tests/test_views.py

@@ -18,7 +18,7 @@ from dcim.constants import *
 from dcim.models import *
 from extras.models import ConfigTemplate
 from ipam.models import ASN, RIR, VLAN, VRF
-from netbox.choices import CSVDelimiterChoices, ImportFormatChoices, WeightUnitChoices
+from netbox.choices import CSVDelimiterChoices, DiameterUnitChoices, ImportFormatChoices, WeightUnitChoices
 from tenancy.models import Tenant
 from users.models import ObjectPermission, User
 from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data
@@ -4236,7 +4236,8 @@ class CoolingPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
             'name': 'Cooling Port Template X',
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
-            'diameter': CoolingDiameterChoices.DN25,
+            'diameter': Decimal('25'),
+            'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'maximum_flow': 100,
             'heat_capacity': 50,
         }
@@ -4246,7 +4247,8 @@ class CoolingPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
             'name': 'Cooling Port Template [4-6]',
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
-            'diameter': CoolingDiameterChoices.DN25,
+            'diameter': Decimal('25'),
+            'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'maximum_flow': 100,
             'heat_capacity': 50,
         }
@@ -4254,7 +4256,8 @@ class CoolingPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestC
         cls.bulk_edit_data = {
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
-            'diameter': CoolingDiameterChoices.DN25,
+            'diameter': Decimal('25'),
+            'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'maximum_flow': 100,
             'heat_capacity': 50,
         }
@@ -4285,7 +4288,8 @@ class CoolingOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTes
             'name': 'Cooling Outlet Template X',
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
-            'diameter': CoolingDiameterChoices.DN25,
+            'diameter': Decimal('25'),
+            'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'cooling_port': coolingports[0].pk,
         }
 
@@ -4294,14 +4298,16 @@ class CoolingOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTes
             'name': 'Cooling Outlet Template [4-6]',
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
-            'diameter': CoolingDiameterChoices.DN25,
+            'diameter': Decimal('25'),
+            'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'cooling_port': coolingports[0].pk,
         }
 
         cls.bulk_edit_data = {
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
-            'diameter': CoolingDiameterChoices.DN25,
+            'diameter': Decimal('25'),
+            'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
         }
 
 
@@ -4327,7 +4333,8 @@ class CoolingPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
             'name': 'Cooling Port X',
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
-            'diameter': CoolingDiameterChoices.DN25,
+            'diameter': Decimal('25'),
+            'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'maximum_flow': 100,
             'heat_capacity': 50,
             'description': 'A cooling port',
@@ -4339,7 +4346,8 @@ class CoolingPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
             'name': 'Cooling Port [4-6]]',
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
-            'diameter': CoolingDiameterChoices.DN25,
+            'diameter': Decimal('25'),
+            'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'maximum_flow': 100,
             'heat_capacity': 50,
             'description': 'A cooling port',
@@ -4349,7 +4357,8 @@ class CoolingPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
         cls.bulk_edit_data = {
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
-            'diameter': CoolingDiameterChoices.DN25,
+            'diameter': Decimal('25'),
+            'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'maximum_flow': 100,
             'heat_capacity': 50,
             'description': 'New description',
@@ -4415,7 +4424,8 @@ class CoolingOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
             'name': 'Cooling Outlet X',
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
-            'diameter': CoolingDiameterChoices.DN25,
+            'diameter': Decimal('25'),
+            'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'cooling_port': coolingports[1].pk,
             'description': 'A cooling outlet',
             'tags': [t.pk for t in tags],
@@ -4426,7 +4436,8 @@ class CoolingOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
             'name': 'Cooling Outlet [4-6]',
             'type': CoolingFeedTypeChoices.TYPE_SUPPLY,
             'connector_type': CoolingConnectorTypeChoices.TYPE_UQD,
-            'diameter': CoolingDiameterChoices.DN25,
+            'diameter': Decimal('25'),
+            'diameter_unit': DiameterUnitChoices.UNIT_MILLIMETER,
             'cooling_port': coolingports[1].pk,
             'description': 'A cooling outlet',
             'tags': [t.pk for t in tags],

+ 2 - 2
netbox/dcim/ui/panels.py

@@ -250,7 +250,7 @@ class CoolingPortPanel(panels.ObjectAttributesPanel):
     label = attrs.TextAttr('label')
     type = attrs.ChoiceAttr('type')
     connector_type = attrs.ChoiceAttr('connector_type')
-    diameter = attrs.ChoiceAttr('diameter')
+    diameter = attrs.NumericAttr('diameter', unit_accessor='get_diameter_unit_display')
     description = attrs.TextAttr('description')
     maximum_flow = attrs.TextAttr('maximum_flow', format_string=_('{} L/min'))
     heat_capacity = attrs.TextAttr('heat_capacity', format_string=_('{} kW'))
@@ -263,7 +263,7 @@ class CoolingOutletPanel(panels.ObjectAttributesPanel):
     label = attrs.TextAttr('label')
     type = attrs.ChoiceAttr('type')
     connector_type = attrs.ChoiceAttr('connector_type')
-    diameter = attrs.ChoiceAttr('diameter')
+    diameter = attrs.NumericAttr('diameter', unit_accessor='get_diameter_unit_display')
     description = attrs.TextAttr('description')
     color = attrs.ColorAttr('color')
     cooling_port = attrs.RelatedObjectAttr('cooling_port', linkify=True)

+ 17 - 0
netbox/netbox/choices.py

@@ -7,6 +7,7 @@ __all__ = (
     'ButtonColorChoices',
     'CSVDelimiterChoices',
     'ColorChoices',
+    'DiameterUnitChoices',
     'DistanceUnitChoices',
     'ImportFormatChoices',
     'ImportMethodChoices',
@@ -197,3 +198,19 @@ class WeightUnitChoices(ChoiceSet):
         (UNIT_POUND, _('Pounds')),
         (UNIT_OUNCE, _('Ounces')),
     )
+
+
+class DiameterUnitChoices(ChoiceSet):
+
+    # Metric
+    UNIT_MILLIMETER = 'mm'
+    UNIT_CENTIMETER = 'cm'
+
+    # Imperial
+    UNIT_INCH = 'in'
+
+    CHOICES = (
+        (UNIT_MILLIMETER, _('Millimeters')),
+        (UNIT_CENTIMETER, _('Centimeters')),
+        (UNIT_INCH, _('Inches')),
+    )

+ 54 - 1
netbox/netbox/models/mixins.py

@@ -3,9 +3,10 @@ from django.db import models
 from django.utils.translation import gettext_lazy as _
 
 from netbox.choices import *
-from utilities.conversion import to_grams, to_meters
+from utilities.conversion import to_grams, to_meters, to_millimeters
 
 __all__ = (
+    'DiameterMixin',
     'DistanceMixin',
     'OwnerMixin',
     'WeightMixin',
@@ -75,6 +76,58 @@ class WeightMixin(models.Model):
             raise ValidationError(_("Must specify a unit when setting a weight"))
 
 
+class DiameterMixin(models.Model):
+    diameter = models.DecimalField(
+        verbose_name=_('diameter'),
+        max_digits=8,
+        decimal_places=2,
+        blank=True,
+        null=True,
+    )
+    diameter_unit = models.CharField(
+        verbose_name=_('diameter unit'),
+        max_length=50,
+        choices=DiameterUnitChoices,
+        blank=True,
+        null=True,
+    )
+    # Stores the normalized diameter (in millimeters) for database ordering
+    _abs_diameter = models.DecimalField(
+        max_digits=13,
+        decimal_places=4,
+        blank=True,
+        null=True
+    )
+
+    class Meta:
+        abstract = True
+
+    @property
+    def abs_diameter(self):
+        # Public alias for _abs_diameter; Django templates cannot access underscore-prefixed attributes.
+        return self._abs_diameter
+
+    def save(self, *args, **kwargs):
+        # Store the given diameter (if any) in millimeters for use in database ordering
+        if self.diameter is not None and self.diameter_unit:
+            self._abs_diameter = to_millimeters(self.diameter, self.diameter_unit)
+        else:
+            self._abs_diameter = None
+
+        # Clear diameter_unit if no diameter is defined
+        if self.diameter is None:
+            self.diameter_unit = None
+
+        super().save(*args, **kwargs)
+
+    def clean(self):
+        super().clean()
+
+        # Validate diameter and diameter_unit
+        if self.diameter and not self.diameter_unit:
+            raise ValidationError(_("Must specify a unit when setting a diameter"))
+
+
 class DistanceMixin(models.Model):
     distance = models.DecimalField(
         verbose_name=_('distance'),

+ 27 - 1
netbox/utilities/conversion.py

@@ -3,11 +3,12 @@ from decimal import Decimal, InvalidOperation
 from django.utils.translation import gettext as _
 
 from dcim.choices import CableLengthUnitChoices
-from netbox.choices import WeightUnitChoices
+from netbox.choices import DiameterUnitChoices, WeightUnitChoices
 
 __all__ = (
     'to_grams',
     'to_meters',
+    'to_millimeters',
 )
 
 
@@ -66,3 +67,28 @@ def to_meters(length, unit) -> Decimal:
             valid_units=', '.join(CableLengthUnitChoices.values())
         )
     )
+
+
+def to_millimeters(diameter, unit) -> Decimal:
+    """
+    Convert the given diameter to millimeters, returning a Decimal value.
+    """
+    try:
+        diameter = Decimal(diameter)
+    except InvalidOperation:
+        raise TypeError(_("Invalid value '{diameter}' for diameter (must be a number)").format(diameter=diameter))
+    if diameter < 0:
+        raise ValueError(_("Diameter must be a positive number"))
+
+    if unit == DiameterUnitChoices.UNIT_MILLIMETER:
+        return round(Decimal(diameter), 4)
+    if unit == DiameterUnitChoices.UNIT_CENTIMETER:
+        return round(Decimal(diameter * 10), 4)
+    if unit == DiameterUnitChoices.UNIT_INCH:
+        return round(diameter * Decimal(25.4), 4)
+    raise ValueError(
+        _("Unknown unit {unit}. Must be one of the following: {valid_units}").format(
+            unit=unit,
+            valid_units=', '.join(DiameterUnitChoices.values())
+        )
+    )