Sfoglia il codice sorgente

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

Jeremy Stretch 1 mese fa
parent
commit
839259ccec

+ 21 - 18
docs/plugins/development/ui-components.md

@@ -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.
 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.ObjectAttribute
 
 
 ::: netbox.ui.attrs.AddressAttr
 ::: netbox.ui.attrs.AddressAttr
 
 
+::: netbox.ui.attrs.ArrayAttr
+
 ::: netbox.ui.attrs.BooleanAttr
 ::: netbox.ui.attrs.BooleanAttr
 
 
 ::: netbox.ui.attrs.ChoiceAttr
 ::: netbox.ui.attrs.ChoiceAttr

+ 24 - 0
netbox/netbox/tests/test_ui.py

@@ -3,6 +3,7 @@ from types import SimpleNamespace
 
 
 from django.template import Context, Template
 from django.template import Context, Template
 from django.test import RequestFactory, SimpleTestCase, TestCase
 from django.test import RequestFactory, SimpleTestCase, TestCase
+from netaddr import IPNetwork
 
 
 from circuits.choices import CircuitStatusChoices, VirtualCircuitTerminationRoleChoices
 from circuits.choices import CircuitStatusChoices, VirtualCircuitTerminationRoleChoices
 from circuits.models import (
 from circuits.models import (
@@ -250,6 +251,29 @@ class TextAttrTestCase(TestCase):
         self.assertTrue(context['copy_button'])
         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):
 class NumericAttrTestCase(TestCase):
 
 
     def test_get_context_with_unit_accessor(self):
     def test_get_context_with_unit_accessor(self):

+ 17 - 0
netbox/netbox/ui/attrs.py

@@ -8,6 +8,7 @@ from utilities.data import resolve_attr_path
 
 
 __all__ = (
 __all__ = (
     'AddressAttr',
     'AddressAttr',
+    'ArrayAttr',
     'BooleanAttr',
     'BooleanAttr',
     'ChoiceAttr',
     'ChoiceAttr',
     'ColorAttr',
     '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):
 class NumericAttr(ObjectAttribute):
     """
     """
     An integer or float attribute.
     An integer or float attribute.