diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index b6e4412285d..dac97b3818f 100644 --- a/netbox/netbox/tables/columns.py +++ b/netbox/netbox/tables/columns.py @@ -539,9 +539,37 @@ def render(self, value): 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) + has_color = False + if not value: + return "" + 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)) + else: + 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 aa6fd5ad2c9..68406e22a07 100644 --- a/netbox/netbox/tests/test_tables.py +++ b/netbox/netbox/tests/test_tables.py @@ -4,6 +4,9 @@ from dcim.models import Device, Site from dcim.tables import DeviceTable +from extras.choices import CustomFieldChoiceColorChoices, CustomFieldTypeChoices +from extras.models import CustomField, CustomFieldChoiceSet +from core.models import ObjectType from netbox.tables import NetBoxTable, columns from utilities.testing import create_tags, create_test_device, create_test_user @@ -119,3 +122,127 @@ def test_tagcolumn(self): '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 one uncolored choice + cls.mixed_choice_set = CustomFieldChoiceSet.objects.create( + name="Mixed Choice Set", + extra_choices=( + ("a", "Option A"), + ("b", "Option B"), + ), + choice_colors={ + "a": CustomFieldChoiceColorChoices.RED, + }, + ) + + # Choice set with no configured colors + cls.plain_choice_set = CustomFieldChoiceSet.objects.create( + name="Plain Choice Set", + extra_choices=( + ("a", "Option A"), + ("b", "Option B"), + ), + ) + + 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]) + + cls.plain_multiselect_cf = CustomField.objects.create( + name="plain_multiselect_field", + type=CustomFieldTypeChoices.TYPE_MULTISELECT, + choice_set=cls.plain_choice_set, + required=False, + ) + cls.plain_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_colored_choices(self): + column = columns.CustomFieldColumn(self.plain_multiselect_cf) + + rendered = str(column.render(["a", "b"])) + + self.assertEqual(rendered, "Option A, Option B") + 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_choice_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_select_field", + type=CustomFieldTypeChoices.TYPE_SELECT, + choice_set=choice_set, + required=False, + ) + custom_field.object_types.set([self.object_type]) + + column = columns.CustomFieldColumn(custom_field) + + rendered = str(column.render("x")) + + self.assertIn("<b>Bold Option</b>", rendered) + self.assertNotIn("&lt;", rendered) \ No newline at end of file