diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index 0d54331d8..08d52d6eb 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -177,29 +177,19 @@ class ModularComponentTemplateModel(ComponentTemplateModel): modules.reverse() return modules - def resolve_name(self, module): - if MODULE_TOKEN not in self.name: - return self.name + def _resolve_module_placeholder(self, value, module): + if MODULE_TOKEN not in value or not module: + return value + modules = self._get_module_tree(module) + for m in modules: + value = value.replace(MODULE_TOKEN, m.module_bay.position, 1) + return value - if module: - modules = self._get_module_tree(module) - name = self.name - for module in modules: - name = name.replace(MODULE_TOKEN, module.module_bay.position, 1) - return name - return self.name + def resolve_name(self, module): + return self._resolve_module_placeholder(self.name, module) def resolve_label(self, module): - if MODULE_TOKEN not in self.label: - return self.label - - if module: - modules = self._get_module_tree(module) - label = self.label - for module in modules: - label = label.replace(MODULE_TOKEN, module.module_bay.position, 1) - return label - return self.label + return self._resolve_module_placeholder(self.label, module) class ConsolePortTemplate(ModularComponentTemplateModel): @@ -729,11 +719,14 @@ class ModuleBayTemplate(ModularComponentTemplateModel): verbose_name = _('module bay template') verbose_name_plural = _('module bay templates') + def resolve_position(self, module): + return self._resolve_module_placeholder(self.position, module) + def instantiate(self, **kwargs): return self.component_model( name=self.resolve_name(kwargs.get('module')), label=self.resolve_label(kwargs.get('module')), - position=self.position, + position=self.resolve_position(kwargs.get('module')), **kwargs ) instantiate.do_not_call_in_templates = True diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 45e07f257..efb2a356e 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -849,6 +849,50 @@ class ModuleBayTestCase(TestCase): nested_bay = module.modulebays.get(name='SFP A-21') self.assertEqual(nested_bay.label, 'A-21') + @tag('regression') # #20467 + def test_nested_module_bay_position_resolution(self): + """Test that {module} in a module bay template's position field is resolved when the module is installed.""" + manufacturer = Manufacturer.objects.first() + site = Site.objects.first() + device_role = DeviceRole.objects.first() + + device_type = DeviceType.objects.create( + manufacturer=manufacturer, + model='Device with Position Test', + slug='device-with-position-test' + ) + ModuleBayTemplate.objects.create( + device_type=device_type, + name='Slot 1', + position='1' + ) + + module_type = ModuleType.objects.create( + manufacturer=manufacturer, + model='Module with Position Placeholder' + ) + ModuleBayTemplate.objects.create( + module_type=module_type, + name='Sub-bay {module}-1', + position='{module}-1' + ) + + device = Device.objects.create( + name='Position Test Device', + device_type=device_type, + role=device_role, + site=site + ) + module_bay = device.modulebays.get(name='Slot 1') + module = Module.objects.create( + device=device, + module_bay=module_bay, + module_type=module_type + ) + + nested_bay = module.modulebays.get(name='Sub-bay 1-1') + self.assertEqual(nested_bay.position, '1-1') + @tag('regression') # #20912 def test_module_bay_parent_cleared_when_module_removed(self): """Test that the parent field is properly cleared when a module bay's module assignment is removed"""