Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
19df80fe77
commit
659d6d1f85
|
|
@ -120,7 +120,7 @@ class CircuitPanel(panels.ObjectAttributesPanel):
|
|||
cid = attrs.TextAttr('cid', label=_('Circuit ID'), style='font-monospace', copy_button=True)
|
||||
type = attrs.RelatedObjectAttr('type', linkify=True, colored=True)
|
||||
status = attrs.ChoiceAttr('status')
|
||||
distance = attrs.NumericAttr('distance', unit_accessor='get_distance_unit_display')
|
||||
distance = attrs.DistanceAttr('distance')
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
install_date = attrs.DateTimeAttr('install_date', spec='date')
|
||||
termination_date = attrs.DateTimeAttr('termination_date', spec='date')
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ CABLE_LENGTH = """
|
|||
|
||||
WEIGHT = """
|
||||
{% load helpers %}
|
||||
{% if value %}{{ value|floatformat:"-2" }} {{ record.weight_unit }}{% endif %}
|
||||
{% display_weight record.weight record.weight_unit record.abs_weight %}
|
||||
"""
|
||||
|
||||
DEVICE_LINK = """
|
||||
|
|
|
|||
|
|
@ -60,9 +60,9 @@ class RackPanel(panels.ObjectAttributesPanel):
|
|||
|
||||
|
||||
class RackWeightPanel(panels.ObjectAttributesPanel):
|
||||
weight = attrs.NumericAttr('weight', unit_accessor='get_weight_unit_display')
|
||||
max_weight = attrs.NumericAttr('max_weight', unit_accessor='get_weight_unit_display', label=_('Maximum weight'))
|
||||
total_weight = attrs.TemplatedAttr('total_weight', template_name='dcim/rack/attrs/total_weight.html')
|
||||
weight = attrs.WeightAttr('weight')
|
||||
max_weight = attrs.WeightAttr('max_weight', label=_('Maximum weight'))
|
||||
total_weight = attrs.TemplatedAttr('total_weight', template_name='dcim/attrs/total_weight.html')
|
||||
|
||||
|
||||
class RackRolePanel(panels.OrganizationalObjectPanel):
|
||||
|
|
@ -137,7 +137,7 @@ class DeviceDeviceTypePanel(panels.ObjectAttributesPanel):
|
|||
class DeviceDimensionsPanel(panels.ObjectAttributesPanel):
|
||||
title = _('Dimensions')
|
||||
|
||||
total_weight = attrs.TemplatedAttr('total_weight', template_name='dcim/device/attrs/total_weight.html')
|
||||
total_weight = attrs.TemplatedAttr('total_weight', template_name='dcim/attrs/total_weight.html')
|
||||
|
||||
|
||||
class DeviceRolePanel(panels.NestedGroupObjectPanel):
|
||||
|
|
@ -155,7 +155,7 @@ class DeviceTypePanel(panels.ObjectAttributesPanel):
|
|||
height = attrs.TemplatedAttr('u_height', template_name='dcim/devicetype/attrs/height.html')
|
||||
exclude_from_utilization = attrs.BooleanAttr('exclude_from_utilization')
|
||||
full_depth = attrs.BooleanAttr('is_full_depth')
|
||||
weight = attrs.NumericAttr('weight', unit_accessor='get_weight_unit_display')
|
||||
weight = attrs.WeightAttr('weight')
|
||||
subdevice_role = attrs.ChoiceAttr('subdevice_role', label=_('Parent/child'))
|
||||
airflow = attrs.ChoiceAttr('airflow')
|
||||
front_image = attrs.ImageAttr('front_image')
|
||||
|
|
@ -184,7 +184,7 @@ class ModuleTypePanel(panels.ObjectAttributesPanel):
|
|||
part_number = attrs.TextAttr('part_number')
|
||||
description = attrs.TextAttr('description')
|
||||
airflow = attrs.ChoiceAttr('airflow')
|
||||
weight = attrs.NumericAttr('weight', unit_accessor='get_weight_unit_display')
|
||||
weight = attrs.WeightAttr('weight')
|
||||
|
||||
|
||||
class PlatformPanel(panels.NestedGroupObjectPanel):
|
||||
|
|
|
|||
|
|
@ -51,6 +51,11 @@ class WeightMixin(models.Model):
|
|||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@property
|
||||
def abs_weight(self):
|
||||
# Public alias for _abs_weight; Django templates cannot access underscore-prefixed attributes.
|
||||
return self._abs_weight
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# Store the given weight (if any) in grams for use in database ordering
|
||||
|
|
@ -95,6 +100,11 @@ class DistanceMixin(models.Model):
|
|||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@property
|
||||
def abs_distance(self):
|
||||
# Public alias for _abs_distance; Django templates cannot access underscore-prefixed attributes.
|
||||
return self._abs_distance
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Store the given distance (if any) in meters for use in database ordering
|
||||
if self.distance is not None and self.distance_unit:
|
||||
|
|
|
|||
|
|
@ -73,6 +73,18 @@ PREFERENCES = {
|
|||
description=_('Render table rows with alternating colors to increase readability'),
|
||||
),
|
||||
|
||||
# Measurements
|
||||
'ui.measurement_system': UserPreference(
|
||||
label=_('Measurement units'),
|
||||
choices=(
|
||||
('', _('Inherited')),
|
||||
('metric', _('Metric')),
|
||||
('imperial', _('Imperial')),
|
||||
),
|
||||
default='',
|
||||
description=_('Preferred unit system for displaying weight and distance measurements'),
|
||||
),
|
||||
|
||||
# Miscellaneous
|
||||
'data_format': UserPreference(
|
||||
label=_('Data format'),
|
||||
|
|
|
|||
|
|
@ -721,7 +721,7 @@ class DistanceColumn(TemplateColumn):
|
|||
"""
|
||||
template_code = """
|
||||
{% load helpers %}
|
||||
{% if record.distance %}{{ record.distance|floatformat:"-2" }} {{ record.distance_unit }}{% endif %}
|
||||
{% display_distance record.distance record.distance_unit record.abs_distance %}
|
||||
"""
|
||||
|
||||
def __init__(self, template_code=template_code, order_by='_abs_distance', **kwargs):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
from decimal import Decimal
|
||||
from types import SimpleNamespace
|
||||
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.template import Context, Template
|
||||
from django.test import RequestFactory, SimpleTestCase, TestCase
|
||||
|
||||
from circuits.choices import CircuitStatusChoices, VirtualCircuitTerminationRoleChoices
|
||||
from circuits.models import (
|
||||
|
|
@ -535,6 +536,254 @@ class DateTimeAttrTestCase(TestCase):
|
|||
self.assertEqual(context['spec'], 'minutes')
|
||||
|
||||
|
||||
class WeightAttrTestCase(SimpleTestCase):
|
||||
|
||||
def _ctx(self, system=''):
|
||||
return {'name': 'weight', 'preferences': {'ui.measurement_system': system}}
|
||||
|
||||
def _obj(self, weight, unit, abs_g, display=None):
|
||||
display_fn = (lambda: display) if display else (lambda: unit)
|
||||
return SimpleNamespace(
|
||||
weight=weight,
|
||||
weight_unit=unit,
|
||||
_abs_weight=abs_g,
|
||||
get_weight_unit_display=display_fn,
|
||||
)
|
||||
|
||||
def test_none_returns_placeholder(self):
|
||||
attr = attrs.WeightAttr('weight')
|
||||
obj = SimpleNamespace(weight=None)
|
||||
self.assertEqual(attr.render(obj, self._ctx()), attr.placeholder)
|
||||
|
||||
def test_inherit_shows_stored_value(self):
|
||||
attr = attrs.WeightAttr('weight')
|
||||
obj = self._obj(5, 'kg', 5000, 'Kilograms')
|
||||
result = attr.render(obj, self._ctx(system=''))
|
||||
self.assertIn('5', result)
|
||||
self.assertIn('kg', result)
|
||||
|
||||
def test_metric_converts_lbs_to_kg(self):
|
||||
# 10 lb = 4535.92 g → 4535.92 / 1000 = 4.54 kg
|
||||
attr = attrs.WeightAttr('weight')
|
||||
obj = self._obj(10, 'lb', 4535.92, 'Pounds')
|
||||
result = attr.render(obj, self._ctx(system='metric'))
|
||||
self.assertIn('4.54', result)
|
||||
self.assertIn('kg', result)
|
||||
|
||||
def test_metric_no_conversion_for_metric_unit(self):
|
||||
attr = attrs.WeightAttr('weight')
|
||||
obj = self._obj(5, 'kg', 5000, 'Kilograms')
|
||||
result = attr.render(obj, self._ctx(system='metric'))
|
||||
self.assertIn('5', result)
|
||||
self.assertIn('kg', result)
|
||||
|
||||
def test_imperial_converts_kg_to_lbs(self):
|
||||
# 1 kg = 1000 g → 1000 / 453.592 = 2.2 lbs
|
||||
attr = attrs.WeightAttr('weight')
|
||||
obj = self._obj(1, 'kg', 1000, 'Kilograms')
|
||||
result = attr.render(obj, self._ctx(system='imperial'))
|
||||
self.assertIn('2.2', result)
|
||||
self.assertIn('lbs', result)
|
||||
|
||||
def test_imperial_converts_kg_to_singular_lb(self):
|
||||
# 453.592 g = exactly 1.0 lb → singular 'lb'
|
||||
attr = attrs.WeightAttr('weight')
|
||||
obj = self._obj(1, 'kg', 453.592, 'Kilograms')
|
||||
result = attr.render(obj, self._ctx(system='imperial'))
|
||||
self.assertIn('1.0', result)
|
||||
self.assertIn('lb', result)
|
||||
self.assertNotIn('lbs', result)
|
||||
|
||||
def test_imperial_no_conversion_for_imperial_unit(self):
|
||||
attr = attrs.WeightAttr('weight')
|
||||
obj = self._obj(10, 'lb', 4535.92, 'Pounds')
|
||||
result = attr.render(obj, self._ctx(system='imperial'))
|
||||
self.assertIn('10', result)
|
||||
self.assertIn('lbs', result)
|
||||
|
||||
def test_metric_no_conversion_when_abs_weight_is_none(self):
|
||||
# abs_weight=None → falsy → falls through to stored value
|
||||
attr = attrs.WeightAttr('weight')
|
||||
obj = SimpleNamespace(weight=10, weight_unit='lb', _abs_weight=None)
|
||||
result = attr.render(obj, self._ctx(system='metric'))
|
||||
self.assertIn('10', result)
|
||||
self.assertIn('lbs', result)
|
||||
|
||||
|
||||
class DistanceAttrTestCase(SimpleTestCase):
|
||||
|
||||
def _ctx(self, system=''):
|
||||
return {'name': 'distance', 'preferences': {'ui.measurement_system': system}}
|
||||
|
||||
def _obj(self, distance, unit, abs_m, display=None):
|
||||
display_fn = (lambda: display) if display else (lambda: unit)
|
||||
return SimpleNamespace(
|
||||
distance=distance,
|
||||
distance_unit=unit,
|
||||
_abs_distance=abs_m,
|
||||
get_distance_unit_display=display_fn,
|
||||
)
|
||||
|
||||
def test_none_returns_placeholder(self):
|
||||
attr = attrs.DistanceAttr('distance')
|
||||
obj = SimpleNamespace(distance=None)
|
||||
self.assertEqual(attr.render(obj, self._ctx()), attr.placeholder)
|
||||
|
||||
def test_inherit_shows_stored_value(self):
|
||||
attr = attrs.DistanceAttr('distance')
|
||||
obj = self._obj(10, 'km', 10000, 'Kilometers')
|
||||
result = attr.render(obj, self._ctx(system=''))
|
||||
self.assertIn('10', result)
|
||||
self.assertIn('km', result)
|
||||
|
||||
def test_metric_converts_ft_to_m_under_threshold(self):
|
||||
# 500 ft = 152.4 m (< 1000) → display in m
|
||||
attr = attrs.DistanceAttr('distance')
|
||||
obj = self._obj(500, 'ft', 152.4, 'Feet')
|
||||
result = attr.render(obj, self._ctx(system='metric'))
|
||||
self.assertIn('152.4', result)
|
||||
self.assertNotIn('km', result)
|
||||
self.assertIn('m', result)
|
||||
|
||||
def test_metric_converts_mi_to_km_over_threshold(self):
|
||||
# 5 mi = 8046.72 m (>= 1000) → 8046.72 / 1000 = 8.05 km
|
||||
attr = attrs.DistanceAttr('distance')
|
||||
obj = self._obj(5, 'mi', 8046.72, 'Miles')
|
||||
result = attr.render(obj, self._ctx(system='metric'))
|
||||
self.assertIn('8.05', result)
|
||||
self.assertIn('km', result)
|
||||
|
||||
def test_imperial_converts_m_to_ft_under_threshold(self):
|
||||
# 500 m (< 1609.344) → 500 / 0.3048 = 1640.42 ft
|
||||
attr = attrs.DistanceAttr('distance')
|
||||
obj = self._obj(500, 'm', 500, 'Meters')
|
||||
result = attr.render(obj, self._ctx(system='imperial'))
|
||||
self.assertIn('1640.42', result)
|
||||
self.assertIn('ft', result)
|
||||
|
||||
def test_imperial_converts_km_to_mi_over_threshold(self):
|
||||
# 10 km = 10000 m (>= 1609.344) → 10000 / 1609.344 = 6.21 mi
|
||||
attr = attrs.DistanceAttr('distance')
|
||||
obj = self._obj(10, 'km', 10000, 'Kilometers')
|
||||
result = attr.render(obj, self._ctx(system='imperial'))
|
||||
self.assertIn('6.21', result)
|
||||
self.assertIn('mi', result)
|
||||
|
||||
def test_metric_no_conversion_for_metric_unit(self):
|
||||
attr = attrs.DistanceAttr('distance')
|
||||
obj = self._obj(10, 'km', 10000, 'Kilometers')
|
||||
result = attr.render(obj, self._ctx(system='metric'))
|
||||
self.assertIn('10', result)
|
||||
self.assertIn('km', result)
|
||||
|
||||
def test_imperial_no_conversion_for_imperial_unit(self):
|
||||
attr = attrs.DistanceAttr('distance')
|
||||
obj = self._obj(10, 'mi', 16093.44, 'Miles')
|
||||
result = attr.render(obj, self._ctx(system='imperial'))
|
||||
self.assertIn('10', result)
|
||||
self.assertIn('mi', result)
|
||||
|
||||
def test_metric_no_conversion_when_abs_distance_is_none(self):
|
||||
# abs_distance=None → falls through to stored value
|
||||
attr = attrs.DistanceAttr('distance')
|
||||
obj = SimpleNamespace(distance=10, distance_unit='ft', _abs_distance=None)
|
||||
result = attr.render(obj, self._ctx(system='metric'))
|
||||
self.assertIn('10', result)
|
||||
self.assertIn('ft', result)
|
||||
|
||||
|
||||
class DisplayWeightTagTestCase(SimpleTestCase):
|
||||
TEMPLATE = Template('{% load helpers %}{% display_weight weight weight_unit abs_weight %}')
|
||||
|
||||
def _render(self, weight, weight_unit, abs_weight, system=''):
|
||||
ctx = Context({
|
||||
'preferences': {'ui.measurement_system': system},
|
||||
'weight': weight,
|
||||
'weight_unit': weight_unit,
|
||||
'abs_weight': abs_weight,
|
||||
})
|
||||
return self.TEMPLATE.render(ctx).strip()
|
||||
|
||||
def test_none_weight_returns_empty(self):
|
||||
self.assertEqual(self._render(None, 'kg', None), '')
|
||||
|
||||
def test_zero_weight_is_not_suppressed(self):
|
||||
self.assertEqual(self._render(0, 'kg', 0), '0 kg')
|
||||
|
||||
def test_inherit_stores_kg(self):
|
||||
self.assertEqual(self._render(5, 'kg', 5000), '5 kg')
|
||||
|
||||
def test_inherit_stores_lb_plural(self):
|
||||
self.assertEqual(self._render(10, 'lb', 4535.92), '10 lbs')
|
||||
|
||||
def test_inherit_stores_lb_singular(self):
|
||||
self.assertEqual(self._render(1, 'lb', 453.592), '1 lb')
|
||||
|
||||
def test_metric_converts_lb_to_kg(self):
|
||||
# 10 lb = 4535.92 g → round(4535.92/1000, 2) = 4.54 kg
|
||||
result = self._render(10, 'lb', 4535.92, system='metric')
|
||||
self.assertEqual(result, '4.54 kg')
|
||||
|
||||
def test_imperial_converts_kg_to_lbs(self):
|
||||
# 1 kg = 1000 g → round(1000/453.592, 2) = 2.2 lbs
|
||||
result = self._render(1, 'kg', 1000, system='imperial')
|
||||
self.assertEqual(result, '2.2 lbs')
|
||||
|
||||
def test_imperial_converts_kg_to_singular_lb(self):
|
||||
# 453.592 g = 1.0 lb → singular
|
||||
result = self._render(1, 'kg', 453.592, system='imperial')
|
||||
self.assertEqual(result, '1 lb')
|
||||
|
||||
def test_metric_no_conversion_for_metric_unit(self):
|
||||
result = self._render(5, 'kg', 5000, system='metric')
|
||||
self.assertEqual(result, '5 kg')
|
||||
|
||||
def test_imperial_no_conversion_for_imperial_unit(self):
|
||||
result = self._render(10, 'lb', 4535.92, system='imperial')
|
||||
self.assertEqual(result, '10 lbs')
|
||||
|
||||
|
||||
class DisplayDistanceTagTestCase(SimpleTestCase):
|
||||
TEMPLATE = Template('{% load helpers %}{% display_distance distance distance_unit abs_distance %}')
|
||||
|
||||
def _render(self, distance, distance_unit, abs_distance, system=''):
|
||||
ctx = Context({
|
||||
'preferences': {'ui.measurement_system': system},
|
||||
'distance': distance,
|
||||
'distance_unit': distance_unit,
|
||||
'abs_distance': abs_distance,
|
||||
})
|
||||
return self.TEMPLATE.render(ctx).strip()
|
||||
|
||||
def test_none_distance_returns_empty(self):
|
||||
self.assertEqual(self._render(None, 'km', None), '')
|
||||
|
||||
def test_inherit_stores_km(self):
|
||||
self.assertEqual(self._render(10, 'km', 10000), '10 km')
|
||||
|
||||
def test_metric_converts_ft_to_m_under_threshold(self):
|
||||
# 500 ft = 152.4 m (< 1000)
|
||||
self.assertEqual(self._render(500, 'ft', 152.4, system='metric'), '152.4 m')
|
||||
|
||||
def test_metric_converts_mi_to_km_over_threshold(self):
|
||||
# 5 mi = 8046.72 m (>= 1000) → 8.05 km
|
||||
self.assertEqual(self._render(5, 'mi', 8046.72, system='metric'), '8.05 km')
|
||||
|
||||
def test_imperial_converts_m_to_ft_under_threshold(self):
|
||||
# 500 m (< 1609.344) → 500/0.3048 = 1640.42 ft
|
||||
self.assertEqual(self._render(500, 'm', 500, system='imperial'), '1640.42 ft')
|
||||
|
||||
def test_imperial_converts_km_to_mi_over_threshold(self):
|
||||
# 10 km = 10000 m (>= 1609.344) → 6.21 mi
|
||||
self.assertEqual(self._render(10, 'km', 10000, system='imperial'), '6.21 mi')
|
||||
|
||||
def test_metric_no_conversion_for_metric_unit(self):
|
||||
self.assertEqual(self._render(10, 'km', 10000, system='metric'), '10 km')
|
||||
|
||||
def test_imperial_no_conversion_for_imperial_unit(self):
|
||||
self.assertEqual(self._render(10, 'mi', 16093.44, system='imperial'), '10 mi')
|
||||
|
||||
|
||||
class ObjectsTablePanelTestCase(TestCase):
|
||||
"""
|
||||
Verify that ObjectsTablePanel.should_render() hides the panel when
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ __all__ = (
|
|||
'ChoiceAttr',
|
||||
'ColorAttr',
|
||||
'DateTimeAttr',
|
||||
'DistanceAttr',
|
||||
'GPSCoordinatesAttr',
|
||||
'GenericForeignKeyAttr',
|
||||
'ImageAttr',
|
||||
|
|
@ -24,6 +25,7 @@ __all__ = (
|
|||
'TextAttr',
|
||||
'TimezoneAttr',
|
||||
'UtilizationAttr',
|
||||
'WeightAttr',
|
||||
)
|
||||
|
||||
PLACEHOLDER_HTML = '<span class="text-muted">—</span>'
|
||||
|
|
@ -569,3 +571,110 @@ class UtilizationAttr(ObjectAttribute):
|
|||
Renders the value of an attribute as a utilization graph.
|
||||
"""
|
||||
template_name = 'ui/attrs/utilization.html'
|
||||
|
||||
|
||||
IMPERIAL_WEIGHT = {'lb', 'oz'}
|
||||
METRIC_WEIGHT = {'kg', 'g'}
|
||||
IMPERIAL_DISTANCE = {'mi', 'ft'}
|
||||
METRIC_DISTANCE = {'km', 'm'}
|
||||
|
||||
|
||||
def compute_weight_display(weight, weight_unit, abs_weight, system):
|
||||
"""
|
||||
Return (display_value, display_unit) for a weight, respecting the user's measurement system.
|
||||
abs_weight is in grams (from WeightMixin._abs_weight).
|
||||
oz and g pass through unchanged since there is no cross-system equivalent.
|
||||
"""
|
||||
if system == 'metric' and weight_unit in IMPERIAL_WEIGHT and abs_weight is not None:
|
||||
return round(abs_weight / 1000, 2), 'kg'
|
||||
if system == 'imperial' and weight_unit in METRIC_WEIGHT and abs_weight is not None:
|
||||
lbs = round(abs_weight / 453.592, 2)
|
||||
return lbs, 'lb' if lbs == 1 else 'lbs'
|
||||
if weight_unit == 'lb':
|
||||
return weight, 'lb' if weight == 1 else 'lbs'
|
||||
return weight, weight_unit
|
||||
|
||||
|
||||
def compute_distance_display(distance, distance_unit, abs_distance, system):
|
||||
"""
|
||||
Return (display_value, display_unit) for a distance, respecting the user's measurement system.
|
||||
abs_distance is in metres (from DistanceMixin._abs_distance).
|
||||
Distances < 1 km are shown in metres; < 1 mi are shown in feet.
|
||||
"""
|
||||
if system == 'metric' and distance_unit in IMPERIAL_DISTANCE and abs_distance is not None:
|
||||
abs_m = float(abs_distance)
|
||||
if abs_m >= 1000:
|
||||
return round(abs_m / 1000, 2), 'km'
|
||||
return round(abs_m, 2), 'm'
|
||||
if system == 'imperial' and distance_unit in METRIC_DISTANCE and abs_distance is not None:
|
||||
abs_m = float(abs_distance)
|
||||
if abs_m >= 1609.344:
|
||||
return round(abs_m / 1609.344, 2), 'mi'
|
||||
return round(abs_m / 0.3048, 2), 'ft'
|
||||
return distance, distance_unit
|
||||
|
||||
|
||||
class WeightAttr(ObjectAttribute):
|
||||
"""
|
||||
A weight attribute that converts to the user's preferred measurement system.
|
||||
|
||||
Parameters:
|
||||
unit_attr (str): Name of the field holding the weight unit (default: 'weight_unit')
|
||||
abs_attr (str): The internal _abs_weight field name on WeightMixin (stored in grams).
|
||||
Accessed via Python — not subject to Django's template underscore restriction.
|
||||
"""
|
||||
template_name = 'ui/attrs/numeric.html'
|
||||
|
||||
def __init__(self, *args, unit_attr='weight_unit', abs_attr='_abs_weight', **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.unit_attr = unit_attr
|
||||
self.abs_attr = abs_attr
|
||||
|
||||
def render(self, obj, context):
|
||||
weight = resolve_attr_path(obj, self.accessor)
|
||||
if weight is None:
|
||||
return self.placeholder
|
||||
|
||||
system = (context.get('preferences') or {}).get('ui.measurement_system') or ''
|
||||
unit = resolve_attr_path(obj, self.unit_attr)
|
||||
abs_weight = resolve_attr_path(obj, self.abs_attr)
|
||||
display_value, display_unit = compute_weight_display(weight, unit, abs_weight, system)
|
||||
|
||||
return render_to_string(self.template_name, {
|
||||
'name': context['name'],
|
||||
'value': display_value,
|
||||
'unit': display_unit,
|
||||
})
|
||||
|
||||
|
||||
class DistanceAttr(ObjectAttribute):
|
||||
"""
|
||||
A distance attribute that converts to the user's preferred measurement system.
|
||||
|
||||
Parameters:
|
||||
unit_attr (str): Name of the field holding the distance unit (default: 'distance_unit')
|
||||
abs_attr (str): The internal _abs_distance field name on DistanceMixin (stored in metres).
|
||||
Accessed via Python — not subject to Django's template underscore restriction.
|
||||
"""
|
||||
template_name = 'ui/attrs/numeric.html'
|
||||
|
||||
def __init__(self, *args, unit_attr='distance_unit', abs_attr='_abs_distance', **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.unit_attr = unit_attr
|
||||
self.abs_attr = abs_attr
|
||||
|
||||
def render(self, obj, context):
|
||||
distance = resolve_attr_path(obj, self.accessor)
|
||||
if distance is None:
|
||||
return self.placeholder
|
||||
|
||||
system = (context.get('preferences') or {}).get('ui.measurement_system') or ''
|
||||
unit = resolve_attr_path(obj, self.unit_attr)
|
||||
abs_distance = resolve_attr_path(obj, self.abs_attr)
|
||||
display_value, display_unit = compute_distance_display(distance, unit, abs_distance, system)
|
||||
|
||||
return render_to_string(self.template_name, {
|
||||
'name': context['name'],
|
||||
'value': display_value,
|
||||
'unit': display_unit,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -205,7 +205,11 @@ class ObjectAttributesPanel(ObjectPanel, metaclass=ObjectAttributesPanelMeta):
|
|||
'attrs': [
|
||||
{
|
||||
'label': attr.label or self._name_to_label(name),
|
||||
'value': attr.render(ctx['object'], {'name': name, 'perms': ctx['perms']}),
|
||||
'value': attr.render(ctx['object'], {
|
||||
'name': name,
|
||||
'perms': ctx['perms'],
|
||||
'preferences': context.get('preferences', {}),
|
||||
}),
|
||||
} for name, attr in self._attrs.items() if name in attr_names
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
{% load helpers i18n %}
|
||||
{% with system=preferences|get_key:"ui.measurement_system" %}
|
||||
{% if system == "imperial" %}
|
||||
{% with lbs=value|kg_to_pounds|floatformat %}{{ lbs }} {% trans "Pound" %}{{ lbs|pluralize }}{% endwith %}
|
||||
{% elif system == "metric" %}
|
||||
{% with formatted=value|floatformat %}{{ formatted }} {% trans "Kilogram" %}{{ formatted|pluralize }}{% endwith %}
|
||||
{% else %}
|
||||
{% with formatted=value|floatformat %}{% with lbs=value|kg_to_pounds|floatformat %}{{ formatted }} {% trans "Kilogram" %}{{ formatted|pluralize }} ({{ lbs }} {% trans "Pound" %}{{ lbs|pluralize }}){% endwith %}{% endwith %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
{% load helpers i18n %}
|
||||
{{ value|floatformat }} {% trans "Kilograms" %}
|
||||
({{ value|kg_to_pounds|floatformat }} {% trans "Pounds" %})
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
{% load helpers i18n %}
|
||||
{{ value|floatformat }} {% trans "Kilograms" %}
|
||||
({{ value|kg_to_pounds|floatformat }} {% trans "Pounds" %})
|
||||
|
|
@ -72,7 +72,7 @@ class UserConfigForm(forms.ModelForm, metaclass=UserConfigFormMetaclass):
|
|||
fieldsets = (
|
||||
FieldSet(
|
||||
'locale.language', 'ui.copilot_enabled', 'pagination.per_page', 'pagination.placement',
|
||||
'ui.tables.striping', name=_('User Interface')
|
||||
'ui.tables.striping', 'ui.measurement_system', name=_('User Interface')
|
||||
),
|
||||
FieldSet('data_format', 'csv_delimiter', name=_('Miscellaneous')),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
from core.models import ObjectType
|
||||
from netbox.settings import DISK_BASE_UNIT, RAM_BASE_UNIT
|
||||
from netbox.ui.attrs import (
|
||||
compute_distance_display,
|
||||
compute_weight_display,
|
||||
)
|
||||
from utilities.forms import TableConfigForm, get_selected_values
|
||||
from utilities.forms.mixins import FORM_FIELD_LOOKUPS
|
||||
from utilities.views import get_action_url, get_viewname
|
||||
|
|
@ -18,6 +22,8 @@ __all__ = (
|
|||
'action_url',
|
||||
'applied_filters',
|
||||
'as_range',
|
||||
'display_distance',
|
||||
'display_weight',
|
||||
'divide',
|
||||
'get_item',
|
||||
'get_key',
|
||||
|
|
@ -332,6 +338,30 @@ def kg_to_pounds(n):
|
|||
return float(n) * 2.204623
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def display_weight(context, weight, weight_unit, abs_weight):
|
||||
"""
|
||||
Render a weight value respecting the user's ui.measurement_system preference.
|
||||
"""
|
||||
if weight is None:
|
||||
return ''
|
||||
system = (context.get('preferences') or {}).get('ui.measurement_system') or ''
|
||||
value, unit = compute_weight_display(weight, weight_unit, abs_weight, system)
|
||||
return f'{value:g} {unit}'
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def display_distance(context, distance, distance_unit, abs_distance):
|
||||
"""
|
||||
Render a distance value respecting the user's ui.measurement_system preference.
|
||||
"""
|
||||
if distance is None:
|
||||
return ''
|
||||
system = (context.get('preferences') or {}).get('ui.measurement_system') or ''
|
||||
value, unit = compute_distance_display(distance, distance_unit, abs_distance, system)
|
||||
return f'{value:g} {unit}'
|
||||
|
||||
|
||||
@register.filter("startswith")
|
||||
def startswith(text: str, starts: str) -> bool:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -48,4 +48,4 @@ class WirelessLinkPropertiesPanel(panels.ObjectAttributesPanel):
|
|||
ssid = attrs.TextAttr('ssid', label=_('SSID'))
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
description = attrs.TextAttr('description')
|
||||
distance = attrs.NumericAttr('distance', unit_accessor='get_distance_unit_display')
|
||||
distance = attrs.DistanceAttr('distance')
|
||||
|
|
|
|||
Loading…
Reference in New Issue