Closes #22361: Introduce ArrayAttr UI panel attribute (#22362)

This commit is contained in:
Jeremy Stretch 2026-06-02 12:17:02 -04:00 committed by GitHub
parent fc17d468aa
commit 839259ccec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 62 additions and 18 deletions

View File

@ -135,29 +135,32 @@ panels.ObjectsTablePanel(
The following classes are available to represent object attributes within an ObjectAttributesPanel. Additionally, plugins can subclass `netbox.ui.attrs.ObjectAttribute` to create custom classes.
| Class | Description |
|------------------------------------------|--------------------------------------------------|
| `netbox.ui.attrs.AddressAttr` | A physical or mailing address. |
| `netbox.ui.attrs.BooleanAttr` | A boolean value |
| `netbox.ui.attrs.ChoiceAttr` | A selection from a set of choices |
| `netbox.ui.attrs.ColorAttr` | A color expressed in RGB |
| `netbox.ui.attrs.DateTimeAttr` | A date or datetime value |
| `netbox.ui.attrs.GenericForeignKeyAttr` | A related object via a generic foreign key |
| `netbox.ui.attrs.GPSCoordinatesAttr` | GPS coordinates (latitude and longitude) |
| `netbox.ui.attrs.ImageAttr` | An attached image (displays the image) |
| `netbox.ui.attrs.NestedObjectAttr` | A related nested object (includes ancestors) |
| `netbox.ui.attrs.NumericAttr` | An integer or float value |
| `netbox.ui.attrs.RelatedObjectAttr` | A related object |
| `netbox.ui.attrs.RelatedObjectListAttr` | A list of related objects |
| `netbox.ui.attrs.TemplatedAttr` | Renders an attribute using a custom template |
| `netbox.ui.attrs.TextAttr` | A string (text) value |
| `netbox.ui.attrs.TimezoneAttr` | A timezone with annotated offset |
| `netbox.ui.attrs.UtilizationAttr` | A numeric value expressed as a utilization graph |
| Class | Description |
|-----------------------------------------|-----------------------------------------------------|
| `netbox.ui.attrs.AddressAttr` | A physical or mailing address. |
| `netbox.ui.attrs.ArrayAttr` | An array of values, shown as a comma-separated list |
| `netbox.ui.attrs.BooleanAttr` | A boolean value |
| `netbox.ui.attrs.ChoiceAttr` | A selection from a set of choices |
| `netbox.ui.attrs.ColorAttr` | A color expressed in RGB |
| `netbox.ui.attrs.DateTimeAttr` | A date or datetime value |
| `netbox.ui.attrs.GenericForeignKeyAttr` | A related object via a generic foreign key |
| `netbox.ui.attrs.GPSCoordinatesAttr` | GPS coordinates (latitude and longitude) |
| `netbox.ui.attrs.ImageAttr` | An attached image (displays the image) |
| `netbox.ui.attrs.NestedObjectAttr` | A related nested object (includes ancestors) |
| `netbox.ui.attrs.NumericAttr` | An integer or float value |
| `netbox.ui.attrs.RelatedObjectAttr` | A related object |
| `netbox.ui.attrs.RelatedObjectListAttr` | A list of related objects |
| `netbox.ui.attrs.TemplatedAttr` | Renders an attribute using a custom template |
| `netbox.ui.attrs.TextAttr` | A string (text) value |
| `netbox.ui.attrs.TimezoneAttr` | A timezone with annotated offset |
| `netbox.ui.attrs.UtilizationAttr` | A numeric value expressed as a utilization graph |
::: netbox.ui.attrs.ObjectAttribute
::: netbox.ui.attrs.AddressAttr
::: netbox.ui.attrs.ArrayAttr
::: netbox.ui.attrs.BooleanAttr
::: netbox.ui.attrs.ChoiceAttr

View File

@ -3,6 +3,7 @@ from types import SimpleNamespace
from django.template import Context, Template
from django.test import RequestFactory, SimpleTestCase, TestCase
from netaddr import IPNetwork
from circuits.choices import CircuitStatusChoices, VirtualCircuitTerminationRoleChoices
from circuits.models import (
@ -250,6 +251,29 @@ class TextAttrTestCase(TestCase):
self.assertTrue(context['copy_button'])
class ArrayAttrTestCase(TestCase):
def test_get_value(self):
attr = attrs.ArrayAttr('allowed_ips')
obj = SimpleNamespace(allowed_ips=[IPNetwork('192.168.1.1/32'), IPNetwork('2001:db8::/64')])
self.assertEqual(attr.get_value(obj), '192.168.1.1/32, 2001:db8::/64')
def test_get_value_empty(self):
attr = attrs.ArrayAttr('allowed_ips')
obj = SimpleNamespace(allowed_ips=[])
self.assertIsNone(attr.get_value(obj))
def test_get_value_none(self):
attr = attrs.ArrayAttr('allowed_ips')
obj = SimpleNamespace(allowed_ips=None)
self.assertIsNone(attr.get_value(obj))
def test_get_value_with_format_string(self):
attr = attrs.ArrayAttr('ports', format_string='{}/tcp')
obj = SimpleNamespace(ports=[80, 443])
self.assertEqual(attr.get_value(obj), '80/tcp, 443/tcp')
class NumericAttrTestCase(TestCase):
def test_get_context_with_unit_accessor(self):

View File

@ -8,6 +8,7 @@ from utilities.data import resolve_attr_path
__all__ = (
'AddressAttr',
'ArrayAttr',
'BooleanAttr',
'ChoiceAttr',
'ColorAttr',
@ -141,6 +142,22 @@ class TextAttr(ObjectAttribute):
}
class ArrayAttr(TextAttr):
"""
An attribute comprising an array of values, rendered as a comma-separated list. If specified, `format_string`
is applied to each item individually. Null and empty arrays are treated as equivalent: both render as the
placeholder.
"""
def get_value(self, obj):
value = resolve_attr_path(obj, self.accessor)
if not value:
return None
if self.format_string:
return ', '.join(self.format_string.format(v) for v in value)
return ', '.join(str(v) for v in value)
class NumericAttr(ObjectAttribute):
"""
An integer or float attribute.