diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 945e442de..4a29792f7 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -186,26 +186,52 @@ def action_url(parser, token): return ActionURLNode(model, action, kwargs, asvar) +def _format_speed(speed, divisor, unit): + """ + Format a speed value with a given divisor and unit. + + Handles decimal values and strips trailing zeros for clean output. + """ + whole, remainder = divmod(speed, divisor) + if remainder == 0: + return f'{whole} {unit}' + + # Divisors are powers of 10, so len(str(divisor)) - 1 matches the decimal precision. + precision = len(str(divisor)) - 1 + fraction = f'{remainder:0{precision}d}'.rstrip('0') + return f'{whole}.{fraction} {unit}' + + @register.filter() def humanize_speed(speed): """ - Humanize speeds given in Kbps. Examples: + Humanize speeds given in Kbps, always using the largest appropriate unit. - 1544 => "1.544 Mbps" - 100000 => "100 Mbps" - 10000000 => "10 Gbps" + Decimal values are displayed when the result is not a whole number; + trailing zeros after the decimal point are stripped for clean output. + + Examples: + + 1_544 => "1.544 Mbps" + 100_000 => "100 Mbps" + 1_000_000 => "1 Gbps" + 2_500_000 => "2.5 Gbps" + 10_000_000 => "10 Gbps" + 800_000_000 => "800 Gbps" + 1_600_000_000 => "1.6 Tbps" """ if not speed: return '' - if speed >= 1000000000 and speed % 1000000000 == 0: - return '{} Tbps'.format(int(speed / 1000000000)) - if speed >= 1000000 and speed % 1000000 == 0: - return '{} Gbps'.format(int(speed / 1000000)) - if speed >= 1000 and speed % 1000 == 0: - return '{} Mbps'.format(int(speed / 1000)) - if speed >= 1000: - return '{} Mbps'.format(float(speed) / 1000) - return '{} Kbps'.format(speed) + + speed = int(speed) + + if speed >= 1_000_000_000: + return _format_speed(speed, 1_000_000_000, 'Tbps') + if speed >= 1_000_000: + return _format_speed(speed, 1_000_000, 'Gbps') + if speed >= 1_000: + return _format_speed(speed, 1_000, 'Mbps') + return f'{speed} Kbps' def _humanize_capacity(value, divisor=1000): diff --git a/netbox/utilities/tests/test_templatetags.py b/netbox/utilities/tests/test_templatetags.py index 570e2595f..5f16649d3 100644 --- a/netbox/utilities/tests/test_templatetags.py +++ b/netbox/utilities/tests/test_templatetags.py @@ -3,7 +3,7 @@ from unittest.mock import patch from django.test import TestCase, override_settings from utilities.templatetags.builtins.tags import static_with_params -from utilities.templatetags.helpers import _humanize_capacity +from utilities.templatetags.helpers import _humanize_capacity, humanize_speed class StaticWithParamsTest(TestCase): @@ -90,3 +90,87 @@ class HumanizeCapacityTest(TestCase): def test_default_divisor_is_1000(self): self.assertEqual(_humanize_capacity(2000), '2.00 GB') + + +class HumanizeSpeedTest(TestCase): + """ + Test the humanize_speed filter for correct unit selection and decimal formatting. + """ + + # Falsy / empty inputs + + def test_none(self): + self.assertEqual(humanize_speed(None), '') + + def test_zero(self): + self.assertEqual(humanize_speed(0), '') + + def test_empty_string(self): + self.assertEqual(humanize_speed(''), '') + + # Kbps (below 1000) + + def test_kbps(self): + self.assertEqual(humanize_speed(100), '100 Kbps') + + def test_kbps_low(self): + self.assertEqual(humanize_speed(1), '1 Kbps') + + # Mbps (1,000 – 999,999) + + def test_mbps_whole(self): + self.assertEqual(humanize_speed(100_000), '100 Mbps') + + def test_mbps_decimal(self): + self.assertEqual(humanize_speed(1_544), '1.544 Mbps') + + def test_mbps_10(self): + self.assertEqual(humanize_speed(10_000), '10 Mbps') + + # Gbps (1,000,000 – 999,999,999) + + def test_gbps_whole(self): + self.assertEqual(humanize_speed(1_000_000), '1 Gbps') + + def test_gbps_decimal(self): + self.assertEqual(humanize_speed(2_500_000), '2.5 Gbps') + + def test_gbps_10(self): + self.assertEqual(humanize_speed(10_000_000), '10 Gbps') + + def test_gbps_25(self): + self.assertEqual(humanize_speed(25_000_000), '25 Gbps') + + def test_gbps_40(self): + self.assertEqual(humanize_speed(40_000_000), '40 Gbps') + + def test_gbps_100(self): + self.assertEqual(humanize_speed(100_000_000), '100 Gbps') + + def test_gbps_400(self): + self.assertEqual(humanize_speed(400_000_000), '400 Gbps') + + def test_gbps_800(self): + self.assertEqual(humanize_speed(800_000_000), '800 Gbps') + + # Tbps (1,000,000,000+) + + def test_tbps_whole(self): + self.assertEqual(humanize_speed(1_000_000_000), '1 Tbps') + + def test_tbps_decimal(self): + self.assertEqual(humanize_speed(1_600_000_000), '1.6 Tbps') + + # Edge cases + + def test_string_input(self): + """Ensure string values are cast to int correctly.""" + self.assertEqual(humanize_speed('2500000'), '2.5 Gbps') + + def test_non_round_remainder_preserved(self): + """Ensure fractional parts with interior zeros are preserved.""" + self.assertEqual(humanize_speed(1_001_000), '1.001 Gbps') + + def test_trailing_zeros_stripped(self): + """Ensure trailing fractional zeros are stripped (5.500 → 5.5).""" + self.assertEqual(humanize_speed(5_500_000), '5.5 Gbps')