#19731: Support module_bay_types in device/module type YAML import and export

Follow-up QA for the ModuleBayType feature added in #22648.

ModuleBayTemplate.to_yaml() omitted module_bay_types, and
ModuleBayTemplateImportForm (used by the DeviceType/ModuleType YAML
"Import Components" flow) didn't expose the field either, so bay-type
constraints could never be defined as part of a device type's YAML
definition -- only assigned by hand, one bay at a time, after import.

Add module_bay_types (by name) to the import form, scoped to the parent
device/module type's manufacturer (or global types) via clean_device_type/
clean_module_type, mirroring the existing scoping pattern used elsewhere in
this form for power_port/cooling_intake. Add it to to_yaml()'s output
symmetrically.
This commit is contained in:
Brian Tiemann 2026-08-13 07:50:59 -04:00
parent f89d3b1f20
commit 157a30ecd7
4 changed files with 48 additions and 1 deletions

View File

@ -1,4 +1,5 @@
from django import forms
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from dcim.choices import InterfacePoEModeChoices, InterfacePoETypeChoices, InterfaceTypeChoices, PortTypeChoices
@ -213,13 +214,37 @@ class PortTemplateMappingImportForm(forms.ModelForm):
class ModuleBayTemplateImportForm(forms.ModelForm):
module_bay_types = forms.ModelMultipleChoiceField(
label=_('Module bay types'),
queryset=ModuleBayType.objects.all(),
to_field_name='name',
required=False,
)
class Meta:
model = ModuleBayTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'position', 'description',
'device_type', 'module_type', 'name', 'label', 'position', 'description', 'module_bay_types',
]
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)
)
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)
)
return module_type
class DeviceBayTemplateImportForm(forms.ModelForm):

View File

@ -988,6 +988,7 @@ class ModuleBayTemplate(ModularComponentTemplateModel):
'position': self.position,
'enabled': self.enabled,
'description': self.description,
'module_bay_types': [t.name for t in self.module_bay_types.all()],
}

View File

@ -182,6 +182,19 @@ class ModuleTypeTestCase(TestCase):
module_type.refresh_from_db()
self.assertEqual(module_type.interface_template_count, 1)
def test_module_bay_template_to_yaml_includes_module_bay_types(self):
"""
ModuleBayTemplate.to_yaml() should export its assigned module bay types by name.
"""
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
module_type = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
bay_type = ModuleBayType.objects.create(name='SFP28', slug='sfp28')
module_bay_template = ModuleBayTemplate.objects.create(module_type=module_type, name='Module Bay 1')
module_bay_template.module_bay_types.set([bay_type])
data = module_bay_template.to_yaml()
self.assertEqual(data['module_bay_types'], ['SFP28'])
def test_attributes(self):
"""
ModuleType.attributes should normalize iterable values into strings for presentation.

View File

@ -998,6 +998,8 @@ port-mappings:
rear_port: Rear Port 3
module-bays:
- name: Module Bay 1
module_bay_types:
- SFP28
- name: Module Bay 2
- name: Module Bay 3
device-bays:
@ -1018,6 +1020,7 @@ inventory-items:
manufacturer.save()
platform = Platform(name='Platform', slug='test-platform', manufacturer=manufacturer)
platform.save()
ModuleBayType.objects.create(name='SFP28', slug='sfp28')
# Add all required permissions to the test user
self.add_permissions(
@ -1126,6 +1129,7 @@ inventory-items:
self.assertEqual(device_type.modulebaytemplates.count(), 3)
mb1 = ModuleBayTemplate.objects.first()
self.assertEqual(mb1.name, 'Module Bay 1')
self.assertEqual(list(mb1.module_bay_types.values_list('name', flat=True)), ['SFP28'])
self.assertEqual(device_type.devicebaytemplates.count(), 3)
db1 = DeviceBayTemplate.objects.first()
@ -1648,6 +1652,8 @@ port-mappings:
module-bays:
- name: Module Bay 1
position: 1
module_bay_types:
- SFP28
- name: Module Bay 2
position: 2
- name: Module Bay 3
@ -1657,6 +1663,7 @@ module-bays:
# Create the manufacturer
manufacturer = Manufacturer(name='Generic', slug='generic')
manufacturer.save()
ModuleBayType.objects.create(name='SFP28', slug='sfp28')
# Add all required permissions to the test user
self.add_permissions(
@ -1752,6 +1759,7 @@ module-bays:
mb1 = ModuleBayTemplate.objects.first()
self.assertEqual(mb1.name, 'Module Bay 1')
self.assertEqual(mb1.position, '1')
self.assertEqual(list(mb1.module_bay_types.values_list('name', flat=True)), ['SFP28'])
@override_settings(STREAMING_EXPORTS=True)
def test_export_objects(self):