diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 6c5ad9249..2fcda5cd1 100644 --- a/netbox/dcim/tables/devices.py +++ b/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): diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py index 427ca0ee4..20bbb38c7 100644 --- a/netbox/dcim/tables/devicetypes.py +++ b/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') diff --git a/netbox/dcim/tables/modules.py b/netbox/dcim/tables/modules.py index 2586b1155..e3706cac3 100644 --- a/netbox/dcim/tables/modules.py +++ b/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, diff --git a/netbox/dcim/tests/test_forms.py b/netbox/dcim/tests/test_forms.py index 559b099d3..dacf7024b 100644 --- a/netbox/dcim/tests/test_forms.py +++ b/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, diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index 314bb7128..5ab71f8d3 100644 --- a/netbox/netbox/tables/columns.py +++ b/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)