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('&lt;', rendered) + self.assertIn('<script>alert('xss')</script>', rendered,) + self.assertNotIn('