diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py
index b6e441228..cd493051a 100644
--- a/netbox/netbox/tables/columns.py
+++ b/netbox/netbox/tables/columns.py
@@ -539,9 +539,37 @@ class CustomFieldColumn(tables.Column):
if self.customfield.type == CustomFieldTypeChoices.TYPE_URL:
return mark_safe(f'{escape(value)}')
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'{escape(label)}'
+ )
+ 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'{escape(label)}'
+ )
+ 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)
diff --git a/netbox/netbox/tests/test_tables.py b/netbox/netbox/tests/test_tables.py
index aa6fd5ad2..9543f7668 100644
--- a/netbox/netbox/tests/test_tables.py
+++ b/netbox/netbox/tests/test_tables.py
@@ -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', 'Bold Option'),
+ ('y', ""),
+ ),
+ 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('<', rendered)
+ self.assertIn('<script>alert('xss')</script>', rendered,)
+ self.assertNotIn('