Render select and multiselect custom field values as colored badges in table views when their associated choices define colors. For multiselect fields, render all selected values as badges when any selected choice has a color, using the secondary badge color for uncolored choices. Preserve comma-separated text when none of the selected choices has a color. Add test coverage for colored, uncolored, empty, mixed, and HTML-sensitive choice values. Co-authored-by: Martin Hauser <mhauser@netboxlabs.com>
This commit is contained in:
parent
749e1b6579
commit
eaa2816964
|
|
@ -539,9 +539,37 @@ class CustomFieldColumn(tables.Column):
|
|||
if self.customfield.type == CustomFieldTypeChoices.TYPE_URL:
|
||||
return mark_safe(f'<a href="{escape(value)}">{escape(value)}</a>')
|
||||
if self.customfield.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||
return self.customfield.get_choice_label(value)
|
||||
if value is None:
|
||||
return self.default
|
||||
label = self.customfield.get_choice_label(value)
|
||||
color = self.customfield.get_choice_color(value)
|
||||
if color:
|
||||
return mark_safe(
|
||||
f'<span class="badge text-bg-{escape(color)}">{escape(label)}</span>'
|
||||
)
|
||||
return label
|
||||
if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
|
||||
return ', '.join(self.customfield.get_choice_label(v) for v in value)
|
||||
if not value:
|
||||
return ''
|
||||
|
||||
has_color = False
|
||||
parts = []
|
||||
|
||||
for v in value:
|
||||
label = self.customfield.get_choice_label(v)
|
||||
color = self.customfield.get_choice_color(v)
|
||||
if color:
|
||||
has_color = True
|
||||
parts.append((label, color))
|
||||
if has_color:
|
||||
badges = []
|
||||
for label, color in parts:
|
||||
badges.append(
|
||||
f'<span class="badge text-bg-{escape(color or "secondary")}">{escape(label)}</span>'
|
||||
)
|
||||
return mark_safe(' '.join(badges))
|
||||
return ', '.join(label for label, _ in parts)
|
||||
|
||||
if self.customfield.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
|
||||
return mark_safe(', '.join(
|
||||
self._linkify_item(obj) for obj in self.customfield.deserialize(value)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,11 @@ from django.contrib.auth.models import AnonymousUser
|
|||
from django.template import Context, Template
|
||||
from django.test import RequestFactory, TestCase
|
||||
|
||||
from core.models import ObjectType
|
||||
from dcim.models import Device, Site
|
||||
from dcim.tables import DeviceTable
|
||||
from extras.choices import CustomFieldChoiceColorChoices, CustomFieldTypeChoices
|
||||
from extras.models import CustomField, CustomFieldChoiceSet
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
from utilities.testing import create_tags, create_test_device, create_test_user
|
||||
|
||||
|
|
@ -119,3 +122,115 @@ class TagColumnTestCase(TestCase):
|
|||
'table': table
|
||||
})
|
||||
template.render(context)
|
||||
|
||||
|
||||
class CustomFieldColumnTestCase(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.object_type = ObjectType.objects.get_for_model(Site)
|
||||
|
||||
# Choice set containing one colored and two uncolored choices
|
||||
cls.mixed_choice_set = CustomFieldChoiceSet.objects.create(
|
||||
name='Mixed Choice Set',
|
||||
extra_choices=(
|
||||
('a', 'Option A'),
|
||||
('b', 'Option B'),
|
||||
('c', 'Option C'),
|
||||
),
|
||||
choice_colors={
|
||||
'a': CustomFieldChoiceColorChoices.RED,
|
||||
},
|
||||
)
|
||||
|
||||
cls.select_cf = CustomField.objects.create(
|
||||
name='select_field',
|
||||
type=CustomFieldTypeChoices.TYPE_SELECT,
|
||||
choice_set=cls.mixed_choice_set,
|
||||
required=False,
|
||||
)
|
||||
cls.select_cf.object_types.set([cls.object_type])
|
||||
|
||||
cls.multiselect_cf = CustomField.objects.create(
|
||||
name='multiselect_field',
|
||||
type=CustomFieldTypeChoices.TYPE_MULTISELECT,
|
||||
choice_set=cls.mixed_choice_set,
|
||||
required=False,
|
||||
)
|
||||
cls.multiselect_cf.object_types.set([cls.object_type])
|
||||
|
||||
def test_colored_single_select(self):
|
||||
column = columns.CustomFieldColumn(self.select_cf)
|
||||
|
||||
rendered = str(column.render('a'))
|
||||
|
||||
self.assertIn('badge', rendered)
|
||||
self.assertIn('text-bg-red', rendered)
|
||||
self.assertIn('Option A', rendered)
|
||||
|
||||
def test_uncolored_single_select(self):
|
||||
column = columns.CustomFieldColumn(self.select_cf)
|
||||
|
||||
rendered = str(column.render('b'))
|
||||
|
||||
self.assertEqual(rendered, 'Option B')
|
||||
self.assertNotIn('badge', rendered)
|
||||
|
||||
def test_empty_multiselect(self):
|
||||
column = columns.CustomFieldColumn(self.multiselect_cf)
|
||||
|
||||
rendered = column.render([])
|
||||
|
||||
self.assertEqual(rendered, '')
|
||||
|
||||
def test_multiselect_without_selected_colored_choices(self):
|
||||
column = columns.CustomFieldColumn(self.multiselect_cf)
|
||||
|
||||
rendered = str(column.render(['b', 'c']))
|
||||
|
||||
self.assertEqual(rendered, 'Option B, Option C')
|
||||
self.assertNotIn('badge', rendered)
|
||||
|
||||
def test_multiselect_with_mixed_colored_choices(self):
|
||||
column = columns.CustomFieldColumn(self.multiselect_cf)
|
||||
|
||||
rendered = str(column.render(['a', 'b']))
|
||||
|
||||
self.assertIn('Option A', rendered)
|
||||
self.assertIn('Option B', rendered)
|
||||
|
||||
self.assertIn('text-bg-red', rendered)
|
||||
self.assertIn('text-bg-secondary', rendered)
|
||||
|
||||
self.assertNotIn(',', rendered)
|
||||
|
||||
def test_html_sensitive_multiselect_labels(self):
|
||||
choice_set = CustomFieldChoiceSet.objects.create(
|
||||
name='HTML Choice Set',
|
||||
extra_choices=(
|
||||
('x', '<b>Bold Option</b>'),
|
||||
('y', "<script>alert('xss')</script>"),
|
||||
),
|
||||
choice_colors={
|
||||
'x': CustomFieldChoiceColorChoices.RED,
|
||||
},
|
||||
)
|
||||
|
||||
custom_field = CustomField.objects.create(
|
||||
name='html_multiselect_field',
|
||||
type=CustomFieldTypeChoices.TYPE_MULTISELECT,
|
||||
choice_set=choice_set,
|
||||
required=False,
|
||||
)
|
||||
custom_field.object_types.set([self.object_type])
|
||||
|
||||
column = columns.CustomFieldColumn(custom_field)
|
||||
|
||||
rendered = str(column.render(['x', 'y']))
|
||||
|
||||
self.assertIn('<b>Bold Option</b>', rendered)
|
||||
self.assertNotIn('&lt;', rendered)
|
||||
self.assertIn('<script>alert('xss')</script>', rendered,)
|
||||
self.assertNotIn('<script>', rendered)
|
||||
|
||||
self.assertIn('text-bg-red', rendered)
|
||||
self.assertIn('text-bg-secondary', rendered)
|
||||
|
|
|
|||
Loading…
Reference in New Issue