Resolve module_bay_types by name deterministically, not via blind filter

ModuleBayType's unique constraint is on (manufacturer, name), not name
alone, so a global type and a manufacturer-scoped type can legally share
the same name. The manufacturer-or-null queryset scoping added for
ModuleBayTemplateImportForm.module_bay_types left both rows in the
filtered queryset in that case, and ModelMultipleChoiceField's default
name-based lookup silently attached both instead of just the one
referenced -- confirmed by reproducing it directly against the form.

Add clean_module_bay_types() to resolve each submitted name explicitly,
preferring a manufacturer-specific match over a global one, and raising a
clear error for an unresolvable name instead of silently under- or
over-matching. Also factor clean_device_type/clean_module_type's
duplicated scoping logic into a shared helper.
This commit is contained in:
Brian Tiemann 2026-08-13 08:21:47 -04:00
parent 157a30ecd7
commit 508e2eaba2
2 changed files with 80 additions and 8 deletions

View File

@ -227,24 +227,49 @@ class ModuleBayTemplateImportForm(forms.ModelForm):
'device_type', 'module_type', 'name', 'label', 'position', 'description', 'module_bay_types',
]
def _scope_module_bay_types(self, manufacturer):
module_bay_types = self.fields['module_bay_types']
module_bay_types.queryset = module_bay_types.queryset.filter(
Q(manufacturer__isnull=True) | Q(manufacturer=manufacturer)
)
def clean_device_type(self):
if device_type := self.cleaned_data['device_type']:
module_bay_types = self.fields['module_bay_types']
module_bay_types.queryset = module_bay_types.queryset.filter(
Q(manufacturer__isnull=True) | Q(manufacturer=device_type.manufacturer)
)
self._scope_module_bay_types(device_type.manufacturer)
return device_type
def clean_module_type(self):
if module_type := self.cleaned_data['module_type']:
module_bay_types = self.fields['module_bay_types']
module_bay_types.queryset = module_bay_types.queryset.filter(
Q(manufacturer__isnull=True) | Q(manufacturer=module_type.manufacturer)
)
self._scope_module_bay_types(module_type.manufacturer)
return module_type
def clean_module_bay_types(self):
"""
Resolve each submitted name against the scoped queryset, preferring a manufacturer-
specific match over a global one when both exist. ModuleBayType's unique constraint
is on (manufacturer, name), not name alone, so a name can legitimately collide between
a global type and one scoped to this template's manufacturer; ModelMultipleChoiceField's
default name-based lookup would otherwise silently attach both.
"""
names = self.data.get('module_bay_types') or []
if not isinstance(names, (list, tuple)):
raise forms.ValidationError(_("Module bay types must be a list."))
queryset = self.fields['module_bay_types'].queryset
resolved = []
for name in names:
candidates = list(queryset.filter(name=name))
if not candidates:
raise forms.ValidationError(
_("Module bay type not found: {name}").format(name=name)
)
match = next((c for c in candidates if c.manufacturer_id is not None), candidates[0])
resolved.append(match)
return resolved
class DeviceBayTemplateImportForm(forms.ModelForm):

View File

@ -229,6 +229,53 @@ class ModuleTypeFormTestCase(TestCase):
self.assertEqual(module_type.attribute_data, {'media': ['copper', 'qsfp28']})
class ModuleBayTemplateImportFormTestCase(TestCase):
def test_module_bay_types_prefers_manufacturer_specific_match_over_global(self):
"""
ModuleBayType's unique constraint is on (manufacturer, name), not name alone, so a
global type and a manufacturer-scoped type can legally share the same name. Referencing
that name by import should resolve to the manufacturer-specific match only, not attach
both.
"""
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
global_type = ModuleBayType.objects.create(name='SFP28', slug='sfp28-global')
scoped_type = ModuleBayType.objects.create(
name='SFP28', slug='sfp28-scoped', manufacturer=manufacturer,
)
device_type = DeviceType.objects.create(
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1',
)
form = ModuleBayTemplateImportForm({
'device_type': device_type.pk,
'name': 'Module Bay 1',
'module_bay_types': ['SFP28'],
})
self.assertTrue(form.is_valid(), form.errors)
module_bay_template = form.save()
self.assertEqual(
list(module_bay_template.module_bay_types.all()), [scoped_type],
)
self.assertNotIn(global_type, module_bay_template.module_bay_types.all())
def test_module_bay_types_unknown_name_raises_error(self):
device_type = DeviceType.objects.create(
manufacturer=Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1'),
model='Device Type 1',
slug='device-type-1',
)
form = ModuleBayTemplateImportForm({
'device_type': device_type.pk,
'name': 'Module Bay 1',
'module_bay_types': ['Nonexistent'],
})
self.assertFalse(form.is_valid())
self.assertIn('module_bay_types', form.errors)
class ModuleFormTestCase(TestCase):
@classmethod