Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions netbox/ipam/models/vlans.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
from dcim.models import Interface, Site, SiteGroup
from ipam.choices import *
from ipam.constants import *
from ipam.querysets import VLANQuerySet, VLANGroupQuerySet
from ipam.querysets import VLANGroupQuerySet, VLANQuerySet
from netbox.models import OrganizationalModel, PrimaryModel, NetBoxModel
from utilities.data import check_ranges_overlap, ranges_to_string
from utilities.data import check_ranges_overlap, ranges_to_string, ranges_to_string_list
from virtualization.models import VMInterface

__all__ = (
Expand Down Expand Up @@ -164,8 +164,18 @@ def get_child_vlans(self):
"""
return VLAN.objects.filter(group=self).order_by('vid')

@property
def vid_ranges_items(self):
"""
Property that converts VID ranges to a list of string representations.
"""
return ranges_to_string_list(self.vid_ranges)

@property
def vid_ranges_list(self):
"""
Property that converts VID ranges into a string representation.
"""
return ranges_to_string(self.vid_ranges)


Expand Down
3 changes: 2 additions & 1 deletion netbox/ipam/tables/vlans.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ class VLANGroupTable(TenancyColumnsMixin, NetBoxTable):
linkify=True,
orderable=False
)
vid_ranges_list = tables.Column(
vid_ranges_list = columns.ArrayColumn(
accessor='vid_ranges_items',
verbose_name=_('VID Ranges'),
orderable=False
)
Expand Down
2 changes: 1 addition & 1 deletion netbox/templates/ipam/vlangroup.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ <h2 class="card-header">{% trans "VLAN Group" %}</h2>
</tr>
<tr>
<th scope="row">{% trans "VLAN IDs" %}</th>
<td>{{ object.vid_ranges_list }}</td>
<td>{{ object.vid_ranges_items|join:", " }}</td>
</tr>
<tr>
<th scope="row">Utilization</th>
Expand Down
43 changes: 33 additions & 10 deletions netbox/utilities/data.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import decimal
from django.db.backends.postgresql.psycopg_any import NumericRange
from itertools import count, groupby

from django.db.backends.postgresql.psycopg_any import NumericRange

__all__ = (
'array_to_ranges',
'array_to_string',
Expand All @@ -10,6 +11,7 @@
'drange',
'flatten_dict',
'ranges_to_string',
'ranges_to_string_list',
'shallow_compare_dict',
'string_to_ranges',
)
Expand Down Expand Up @@ -73,8 +75,10 @@ def shallow_compare_dict(source_dict, destination_dict, exclude=tuple()):
def array_to_ranges(array):
"""
Convert an arbitrary array of integers to a list of consecutive values. Nonconsecutive values are returned as
single-item tuples. For example:
[0, 1, 2, 10, 14, 15, 16] => [(0, 2), (10,), (14, 16)]"
single-item tuples.

Example:
[0, 1, 2, 10, 14, 15, 16] => [(0, 2), (10,), (14, 16)]
"""
group = (
list(x) for _, x in groupby(sorted(array), lambda x, c=count(): next(c) - x)
Expand All @@ -87,7 +91,8 @@ def array_to_ranges(array):
def array_to_string(array):
"""
Generate an efficient, human-friendly string from a set of integers. Intended for use with ArrayField.
For example:

Example:
[0, 1, 2, 10, 14, 15, 16] => "0-2, 10, 14-16"
"""
ret = []
Expand Down Expand Up @@ -135,6 +140,29 @@ def check_ranges_overlap(ranges):
return False


def ranges_to_string_list(ranges):
"""
Convert numeric ranges to a list of display strings.

Each range is rendered as "lower-upper" or "lower" (for singletons).
Bounds are normalized to inclusive values using ``lower_inc``/``upper_inc``.
This underpins ``ranges_to_string()``, which joins the result with commas.

Example:
[NumericRange(1, 6), NumericRange(8, 9), NumericRange(10, 13)] => ["1-5", "8", "10-12"]
"""
if not ranges:
return []

output: list[str] = []
for r in ranges:
# Compute inclusive bounds regardless of how the DB range is stored.
lower = r.lower if r.lower_inc else r.lower + 1
upper = r.upper if r.upper_inc else r.upper - 1
output.append(f"{lower}-{upper}" if lower != upper else str(lower))
return output


def ranges_to_string(ranges):
"""
Converts a list of ranges into a string representation.
Expand All @@ -151,12 +179,7 @@ def ranges_to_string(ranges):
"""
if not ranges:
return ''
output = []
for r in ranges:
lower = r.lower if r.lower_inc else r.lower + 1
upper = r.upper if r.upper_inc else r.upper - 1
output.append(f"{lower}-{upper}" if lower != upper else str(lower))
return ','.join(output)
return ','.join(ranges_to_string_list(ranges))


def string_to_ranges(value):
Expand Down
22 changes: 19 additions & 3 deletions netbox/utilities/tests/test_data.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from django.db.backends.postgresql.psycopg_any import NumericRange
from django.test import TestCase

from utilities.data import check_ranges_overlap, ranges_to_string, string_to_ranges
from utilities.data import (
check_ranges_overlap,
ranges_to_string,
ranges_to_string_list,
string_to_ranges,
)


class RangeFunctionsTestCase(TestCase):
Expand Down Expand Up @@ -47,14 +51,26 @@ def test_check_ranges_overlap(self):
])
)

def test_ranges_to_string_list(self):
self.assertEqual(
ranges_to_string_list([
NumericRange(10, 20), # 10-19
NumericRange(30, 40), # 30-39
NumericRange(50, 51), # 50-50
NumericRange(100, 200), # 100-199
]),
['10-19', '30-39', '50', '100-199']
)

def test_ranges_to_string(self):
self.assertEqual(
ranges_to_string([
NumericRange(10, 20), # 10-19
NumericRange(30, 40), # 30-39
NumericRange(50, 51), # 50-50
NumericRange(100, 200), # 100-199
]),
'10-19,30-39,100-199'
'10-19,30-39,50,100-199'
)

def test_string_to_ranges(self):
Expand Down