فهرست منبع

Fix UI regression: transform= changed the rendered column, not just CSV export

django-tables2's ManyToManyColumn.render() and NetBox's own value() override
both call self.transform() for each item -- there's no built-in way to give
CSV export a different representation than the rendered column. Setting
transform=lambda obj: obj.name on the three module_bay_types columns to fix
CSV export therefore also dropped the manufacturer prefix from the Bay Types
column in the Module Bays, Module Bay Templates, and Module Types list
views -- the opposite of what ModuleBayType.__str__() adds that prefix for.
Verified directly: with the old transform=, two same-named bay types from
different manufacturers render as visually identical "SFP28" list items.

Add export_transform to NetBox's ManyToManyColumn subclass, defaulting to
transform so existing columns are unaffected, and used only by value()
(export) rather than render() (UI). Switch the three columns to
export_transform=lambda obj: obj.name, leaving transform unset so render()
keeps str()'s manufacturer prefix.

Extended the existing round-trip test to also assert the rendered column
still includes the manufacturer name; confirmed it fails against the old
transform= approach and passes with export_transform=.
Brian Tiemann 1 هفته پیش
والد
کامیت
e5c0d60d09

+ 2 - 2
netbox/dcim/tables/devices.py

@@ -1028,8 +1028,8 @@ class ModuleBayTable(ModularDeviceComponentTable):
     module_bay_types = columns.ManyToManyColumn(
         verbose_name=_('Bay Types'),
         linkify_item=True,
-        # __str__() includes the manufacturer, but import resolves by name alone.
-        transform=lambda obj: obj.name,
+        # Export as bare name (import resolves by name alone); UI keeps str()'s manufacturer prefix.
+        export_transform=lambda obj: obj.name,
     )
 
     class Meta(ModularDeviceComponentTable.Meta):

+ 2 - 2
netbox/dcim/tables/devicetypes.py

@@ -305,8 +305,8 @@ class ModuleBayTemplateTable(ComponentTemplateTable):
     module_bay_types = columns.ManyToManyColumn(
         verbose_name=_('Bay Types'),
         linkify_item=True,
-        # __str__() includes the manufacturer, but import resolves by name alone.
-        transform=lambda obj: obj.name,
+        # Export as bare name (import resolves by name alone); UI keeps str()'s manufacturer prefix.
+        export_transform=lambda obj: obj.name,
     )
     actions = columns.ActionsColumn(
         actions=('edit', 'delete')

+ 2 - 2
netbox/dcim/tables/modules.py

@@ -78,8 +78,8 @@ class ModuleTypeTable(PrimaryModelTable):
     module_bay_types = columns.ManyToManyColumn(
         verbose_name=_('Bay Types'),
         linkify_item=True,
-        # __str__() includes the manufacturer, but import resolves by name alone.
-        transform=lambda obj: obj.name,
+        # Export as bare name (import resolves by name alone); UI keeps str()'s manufacturer prefix.
+        export_transform=lambda obj: obj.name,
     )
     model = tables.Column(
         linkify=True,

+ 5 - 3
netbox/dcim/tests/test_forms.py

@@ -516,16 +516,18 @@ class ModuleTypeImportFormTestCase(TestCase):
         self.assertFalse(form.save().module_bay_types.exists())
 
     def test_module_bay_types_round_trips_through_the_table_column_export_value(self):
-        """The table's CSV export (multi-value separator, name-only transform) must be re-importable."""
+        """The table's CSV export (multi-value separator, name-only transform) must be re-importable,
+        without changing the rendered UI column, which should keep str()'s manufacturer prefix."""
         manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
         bay_type_a = ModuleBayType.objects.create(name='SFP28', slug='sfp28', manufacturer=manufacturer)
         bay_type_b = ModuleBayType.objects.create(name='QSFP28', slug='qsfp28', manufacturer=manufacturer)
         original = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
         original.module_bay_types.set([bay_type_a, bay_type_b])
 
-        table = ModuleTypeTable([original])
-        exported_value = table.columns['module_bay_types'].column.value(original.module_bay_types.all())
+        column = ModuleTypeTable([original]).columns['module_bay_types'].column
+        exported_value = column.value(original.module_bay_types.all())
         self.assertEqual(exported_value, 'QSFP28, SFP28')
+        self.assertIn(str(manufacturer), str(column.render(original.module_bay_types.all())))
 
         form = ModuleTypeImportForm({
             'manufacturer': manufacturer.name,

+ 9 - 1
netbox/netbox/tables/columns.py

@@ -131,9 +131,17 @@ class DurationColumn(tables.Column):
 class ManyToManyColumn(tables.ManyToManyColumn):
     """
     Overrides django-tables2's stock ManyToManyColumn to ensure that value() returns only plaintext data.
+
+    export_transform: optional callable used only for value() (CSV/table export), letting export use a
+    different representation than the rendered column (e.g. a bare name where the UI shows str(obj)).
+    Defaults to transform, matching the stock behavior of exporting the same text that's rendered.
     """
+    def __init__(self, *args, export_transform=None, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.export_transform = export_transform or self.transform
+
     def value(self, value):
-        items = [self.transform(item) for item in self.filter(value)]
+        items = [self.export_transform(item) for item in self.filter(value)]
         return self.separator.join(items)