mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-09 13:22:18 -06:00
Compare commits
5 Commits
fix_module
...
20923-dcim
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5471a1f6e | ||
|
|
976b76dcb2 | ||
|
|
06d53ef10b | ||
|
|
eb01f6fde8 | ||
|
|
fba40ddf72 |
@@ -126,26 +126,18 @@ class ModuleCommonForm(forms.Form):
|
||||
_("Cannot install module with placeholder values in a module bay with no position defined.")
|
||||
)
|
||||
|
||||
token_count = template.name.count(MODULE_TOKEN)
|
||||
# Validate: depth must be >= token_count (can't expand tokens without context)
|
||||
if len(module_bays) < token_count:
|
||||
if len(module_bays) != template.name.count(MODULE_TOKEN):
|
||||
raise forms.ValidationError(
|
||||
_(
|
||||
"Cannot install module with placeholder values in a module bay tree {level} in tree "
|
||||
"but {tokens} placeholders given."
|
||||
).format(
|
||||
level=len(module_bays), tokens=token_count
|
||||
level=len(module_bays), tokens=template.name.count(MODULE_TOKEN)
|
||||
)
|
||||
)
|
||||
|
||||
if token_count == 1:
|
||||
# Single token: substitute with full path (e.g., "1/1" for depth 2)
|
||||
full_path = '/'.join([mb.position for mb in module_bays])
|
||||
resolved_name = resolved_name.replace(MODULE_TOKEN, full_path, 1)
|
||||
else:
|
||||
# Multiple tokens: substitute level-by-level (existing behavior)
|
||||
for mb in module_bays:
|
||||
resolved_name = resolved_name.replace(MODULE_TOKEN, mb.position, 1)
|
||||
for module_bay in module_bays:
|
||||
resolved_name = resolved_name.replace(MODULE_TOKEN, module_bay.position, 1)
|
||||
|
||||
existing_item = installed_components.get(resolved_name)
|
||||
|
||||
|
||||
@@ -175,16 +175,9 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
|
||||
|
||||
if module:
|
||||
modules = self._get_module_tree(module)
|
||||
token_count = self.name.count(MODULE_TOKEN)
|
||||
name = self.name
|
||||
if token_count == 1:
|
||||
# Single token: substitute with full path (e.g., "1/1" for depth 2)
|
||||
full_path = '/'.join([m.module_bay.position for m in modules])
|
||||
name = name.replace(MODULE_TOKEN, full_path, 1)
|
||||
else:
|
||||
# Multiple tokens: substitute level-by-level (existing behavior)
|
||||
for m in modules:
|
||||
name = name.replace(MODULE_TOKEN, m.module_bay.position, 1)
|
||||
for module in modules:
|
||||
name = name.replace(MODULE_TOKEN, module.module_bay.position, 1)
|
||||
return name
|
||||
return self.name
|
||||
|
||||
@@ -194,16 +187,9 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
|
||||
|
||||
if module:
|
||||
modules = self._get_module_tree(module)
|
||||
token_count = self.label.count(MODULE_TOKEN)
|
||||
label = self.label
|
||||
if token_count == 1:
|
||||
# Single token: substitute with full path (e.g., "1/1" for depth 2)
|
||||
full_path = '/'.join([m.module_bay.position for m in modules])
|
||||
label = label.replace(MODULE_TOKEN, full_path, 1)
|
||||
else:
|
||||
# Multiple tokens: substitute level-by-level (existing behavior)
|
||||
for m in modules:
|
||||
label = label.replace(MODULE_TOKEN, m.module_bay.position, 1)
|
||||
for module in modules:
|
||||
label = label.replace(MODULE_TOKEN, module.module_bay.position, 1)
|
||||
return label
|
||||
return self.label
|
||||
|
||||
|
||||
@@ -848,720 +848,6 @@ class ModuleBayTestCase(TestCase):
|
||||
nested_bay = module.modulebays.get(name='SFP A-21')
|
||||
self.assertEqual(nested_bay.label, 'A-21')
|
||||
|
||||
def test_nested_module_single_placeholder_full_path(self):
|
||||
"""
|
||||
Test that installing a module at depth=2 with a single {module} placeholder
|
||||
in the interface template name resolves to the full path (e.g., "1/1").
|
||||
Regression test for transceiver modeling use case.
|
||||
"""
|
||||
manufacturer = Manufacturer.objects.first()
|
||||
site = Site.objects.first()
|
||||
device_role = DeviceRole.objects.first()
|
||||
|
||||
# Create device type with module bay template
|
||||
device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='Chassis Device',
|
||||
slug='chassis-device'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
device_type=device_type,
|
||||
name='Line Card Bay 1',
|
||||
position='1'
|
||||
)
|
||||
|
||||
# Create line card module type with nested module bay
|
||||
line_card_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='Line Card'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
module_type=line_card_type,
|
||||
name='SFP Bay {module}/1',
|
||||
label='SFP {module}/1',
|
||||
position='1'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
module_type=line_card_type,
|
||||
name='SFP Bay {module}/2',
|
||||
label='SFP {module}/2',
|
||||
position='2'
|
||||
)
|
||||
|
||||
# Create SFP module type with interface using single {module} placeholder
|
||||
sfp_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='SFP Transceiver'
|
||||
)
|
||||
InterfaceTemplate.objects.create(
|
||||
module_type=sfp_type,
|
||||
name='SFP {module}',
|
||||
label='{module}',
|
||||
type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
|
||||
)
|
||||
|
||||
# Create device
|
||||
device = Device.objects.create(
|
||||
name='Test Chassis',
|
||||
device_type=device_type,
|
||||
role=device_role,
|
||||
site=site
|
||||
)
|
||||
|
||||
# Install line card in bay 1
|
||||
line_card_bay = device.modulebays.get(name='Line Card Bay 1')
|
||||
line_card = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=line_card_bay,
|
||||
module_type=line_card_type
|
||||
)
|
||||
|
||||
# Install SFP in nested bay 1 (depth=2)
|
||||
sfp_bay_1 = line_card.modulebays.get(name='SFP Bay 1/1')
|
||||
sfp_module_1 = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=sfp_bay_1,
|
||||
module_type=sfp_type
|
||||
)
|
||||
|
||||
# Verify interface name resolves to full path "1/1"
|
||||
interface_1 = sfp_module_1.interfaces.first()
|
||||
self.assertEqual(interface_1.name, 'SFP 1/1')
|
||||
self.assertEqual(interface_1.label, '1/1')
|
||||
|
||||
# Install second SFP in nested bay 2 (depth=2) - verifies uniqueness
|
||||
sfp_bay_2 = line_card.modulebays.get(name='SFP Bay 1/2')
|
||||
sfp_module_2 = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=sfp_bay_2,
|
||||
module_type=sfp_type
|
||||
)
|
||||
|
||||
# Verify second interface name resolves to full path "1/2"
|
||||
interface_2 = sfp_module_2.interfaces.first()
|
||||
self.assertEqual(interface_2.name, 'SFP 1/2')
|
||||
self.assertEqual(interface_2.label, '1/2')
|
||||
|
||||
def test_single_placeholder_direct_install_depth_1(self):
|
||||
"""
|
||||
Test that installing a module directly at depth=1 with a single {module}
|
||||
placeholder still resolves correctly (just the position, not a path).
|
||||
"""
|
||||
manufacturer = Manufacturer.objects.first()
|
||||
site = Site.objects.first()
|
||||
device_role = DeviceRole.objects.first()
|
||||
|
||||
# Create device type with module bay template
|
||||
device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='Simple Chassis',
|
||||
slug='simple-chassis'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
device_type=device_type,
|
||||
name='SFP Bay 1',
|
||||
position='1'
|
||||
)
|
||||
|
||||
# Create SFP module type with interface using single {module} placeholder
|
||||
sfp_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='Direct SFP'
|
||||
)
|
||||
InterfaceTemplate.objects.create(
|
||||
module_type=sfp_type,
|
||||
name='SFP {module}',
|
||||
label='{module}',
|
||||
type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
|
||||
)
|
||||
|
||||
# Create device
|
||||
device = Device.objects.create(
|
||||
name='Test Simple Chassis',
|
||||
device_type=device_type,
|
||||
role=device_role,
|
||||
site=site
|
||||
)
|
||||
|
||||
# Install SFP directly in bay 1 (depth=1)
|
||||
sfp_bay = device.modulebays.get(name='SFP Bay 1')
|
||||
sfp_module = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=sfp_bay,
|
||||
module_type=sfp_type
|
||||
)
|
||||
|
||||
# Verify interface name resolves to just "1"
|
||||
interface = sfp_module.interfaces.first()
|
||||
self.assertEqual(interface.name, 'SFP 1')
|
||||
self.assertEqual(interface.label, '1')
|
||||
|
||||
def test_multi_token_level_by_level_depth_2(self):
|
||||
"""
|
||||
T1: Multi-token behavior remains unchanged at depth=2.
|
||||
Ensure legacy {module}/{module} still resolves level-by-level.
|
||||
"""
|
||||
site = Site.objects.create(name='T1 Site', slug='t1-site')
|
||||
manufacturer = Manufacturer.objects.create(name='T1 Manufacturer', slug='t1-manufacturer')
|
||||
device_role = DeviceRole.objects.create(name='T1 Role', slug='t1-role')
|
||||
|
||||
# Create device type with module bay
|
||||
device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='T1 Chassis',
|
||||
slug='t1-chassis'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
device_type=device_type,
|
||||
name='Bay 1',
|
||||
position='1'
|
||||
)
|
||||
|
||||
# Create line card module type with nested bay
|
||||
line_card_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='T1 Line Card'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
module_type=line_card_type,
|
||||
name='Nested Bay 2',
|
||||
position='2'
|
||||
)
|
||||
|
||||
# Create SFP module type with 2-token interface template
|
||||
sfp_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='T1 SFP'
|
||||
)
|
||||
InterfaceTemplate.objects.create(
|
||||
module_type=sfp_type,
|
||||
name='SFP {module}/{module}',
|
||||
type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
|
||||
)
|
||||
|
||||
# Create device and install modules
|
||||
device = Device.objects.create(
|
||||
name='T1 Device',
|
||||
device_type=device_type,
|
||||
role=device_role,
|
||||
site=site
|
||||
)
|
||||
|
||||
# Install line card at position 1
|
||||
line_card_bay = device.modulebays.get(name='Bay 1')
|
||||
line_card = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=line_card_bay,
|
||||
module_type=line_card_type
|
||||
)
|
||||
|
||||
# Install SFP at nested bay (position 2)
|
||||
sfp_bay = line_card.modulebays.get(name='Nested Bay 2')
|
||||
sfp_module = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=sfp_bay,
|
||||
module_type=sfp_type
|
||||
)
|
||||
|
||||
# Verify level-by-level substitution: 1/2 (not 1/2/1/2)
|
||||
interface = sfp_module.interfaces.first()
|
||||
self.assertEqual(interface.name, 'SFP 1/2')
|
||||
|
||||
def test_multi_token_deeper_tree_only_consumes_tokens(self):
|
||||
"""
|
||||
T2: Multi-token with deeper tree only consumes tokens (depth=3, tokens=2).
|
||||
2 tokens → 2 levels, even if tree is deeper.
|
||||
"""
|
||||
site = Site.objects.create(name='T2 Site', slug='t2-site')
|
||||
manufacturer = Manufacturer.objects.create(name='T2 Manufacturer', slug='t2-manufacturer')
|
||||
device_role = DeviceRole.objects.create(name='T2 Role', slug='t2-role')
|
||||
|
||||
# Create device type with module bay
|
||||
device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='T2 Chassis',
|
||||
slug='t2-chassis'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
device_type=device_type,
|
||||
name='Bay 1',
|
||||
position='1'
|
||||
)
|
||||
|
||||
# Create level 2 module type with nested bay
|
||||
level2_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='T2 Level2'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
module_type=level2_type,
|
||||
name='Level2 Bay',
|
||||
position='1'
|
||||
)
|
||||
|
||||
# Create level 3 module type with nested bay
|
||||
level3_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='T2 Level3'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
module_type=level3_type,
|
||||
name='Level3 Bay',
|
||||
position='1'
|
||||
)
|
||||
|
||||
# Create leaf module type with 2-token interface template
|
||||
leaf_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='T2 Leaf'
|
||||
)
|
||||
InterfaceTemplate.objects.create(
|
||||
module_type=leaf_type,
|
||||
name='SFP {module}/{module}',
|
||||
type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
|
||||
)
|
||||
|
||||
# Create device and install 3 levels of modules
|
||||
device = Device.objects.create(
|
||||
name='T2 Device',
|
||||
device_type=device_type,
|
||||
role=device_role,
|
||||
site=site
|
||||
)
|
||||
|
||||
# Level 1
|
||||
bay1 = device.modulebays.get(name='Bay 1')
|
||||
module1 = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=bay1,
|
||||
module_type=level2_type
|
||||
)
|
||||
|
||||
# Level 2
|
||||
bay2 = module1.modulebays.get(name='Level2 Bay')
|
||||
module2 = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=bay2,
|
||||
module_type=level3_type
|
||||
)
|
||||
|
||||
# Level 3 (leaf)
|
||||
bay3 = module2.modulebays.get(name='Level3 Bay')
|
||||
leaf_module = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=bay3,
|
||||
module_type=leaf_type
|
||||
)
|
||||
|
||||
# Verify: 2 tokens → consumes first 2 levels only: "1/1" (not "1/1/1")
|
||||
interface = leaf_module.interfaces.first()
|
||||
self.assertEqual(interface.name, 'SFP 1/1')
|
||||
|
||||
def test_too_many_tokens_fails_validation(self):
|
||||
"""
|
||||
T3: Too-many-tokens still fails (depth=2, tokens=3).
|
||||
Confirms the validation prevents impossible substitution.
|
||||
"""
|
||||
from dcim.forms import ModuleForm
|
||||
|
||||
site = Site.objects.create(name='T3 Site', slug='t3-site')
|
||||
manufacturer = Manufacturer.objects.create(name='T3 Manufacturer', slug='t3-manufacturer')
|
||||
device_role = DeviceRole.objects.create(name='T3 Role', slug='t3-role')
|
||||
|
||||
# Create device type with module bay
|
||||
device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='T3 Chassis',
|
||||
slug='t3-chassis'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
device_type=device_type,
|
||||
name='Bay 1',
|
||||
position='1'
|
||||
)
|
||||
|
||||
# Create line card module type with nested bay
|
||||
line_card_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='T3 Line Card'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
module_type=line_card_type,
|
||||
name='Nested Bay',
|
||||
position='1'
|
||||
)
|
||||
|
||||
# Create leaf module type with 3-token interface template (too many!)
|
||||
leaf_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='T3 Leaf'
|
||||
)
|
||||
InterfaceTemplate.objects.create(
|
||||
module_type=leaf_type,
|
||||
name='{module}/{module}/{module}',
|
||||
type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
|
||||
)
|
||||
|
||||
# Create device and install line card
|
||||
device = Device.objects.create(
|
||||
name='T3 Device',
|
||||
device_type=device_type,
|
||||
role=device_role,
|
||||
site=site
|
||||
)
|
||||
|
||||
bay1 = device.modulebays.get(name='Bay 1')
|
||||
line_card = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=bay1,
|
||||
module_type=line_card_type
|
||||
)
|
||||
|
||||
# Attempt to install leaf module at depth=2 with 3 tokens - should fail
|
||||
nested_bay = line_card.modulebays.get(name='Nested Bay')
|
||||
|
||||
form = ModuleForm(data={
|
||||
'device': device.pk,
|
||||
'module_bay': nested_bay.pk,
|
||||
'module_type': leaf_type.pk,
|
||||
'status': 'active',
|
||||
'replicate_components': True,
|
||||
'adopt_components': False,
|
||||
})
|
||||
|
||||
self.assertFalse(form.is_valid())
|
||||
# Check the error message mentions the mismatch
|
||||
self.assertIn('2', str(form.errors))
|
||||
self.assertIn('3', str(form.errors))
|
||||
|
||||
def test_label_substitution_matches_name_depth_2(self):
|
||||
"""
|
||||
T4: Label substitution works the same way as name (depth=2 single-token).
|
||||
"""
|
||||
site = Site.objects.create(name='T4 Site', slug='t4-site')
|
||||
manufacturer = Manufacturer.objects.create(name='T4 Manufacturer', slug='t4-manufacturer')
|
||||
device_role = DeviceRole.objects.create(name='T4 Role', slug='t4-role')
|
||||
|
||||
# Create device type with module bay
|
||||
device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='T4 Chassis',
|
||||
slug='t4-chassis'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
device_type=device_type,
|
||||
name='Bay 1',
|
||||
position='1'
|
||||
)
|
||||
|
||||
# Create line card module type with nested bay at position 2
|
||||
line_card_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='T4 Line Card'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
module_type=line_card_type,
|
||||
name='Nested Bay',
|
||||
position='2'
|
||||
)
|
||||
|
||||
# Create leaf module type with single-token name AND label
|
||||
leaf_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='T4 Leaf'
|
||||
)
|
||||
InterfaceTemplate.objects.create(
|
||||
module_type=leaf_type,
|
||||
name='SFP {module}',
|
||||
label='LBL {module}',
|
||||
type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
|
||||
)
|
||||
|
||||
# Create device and install modules
|
||||
device = Device.objects.create(
|
||||
name='T4 Device',
|
||||
device_type=device_type,
|
||||
role=device_role,
|
||||
site=site
|
||||
)
|
||||
|
||||
bay1 = device.modulebays.get(name='Bay 1')
|
||||
line_card = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=bay1,
|
||||
module_type=line_card_type
|
||||
)
|
||||
|
||||
nested_bay = line_card.modulebays.get(name='Nested Bay')
|
||||
leaf_module = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=nested_bay,
|
||||
module_type=leaf_type
|
||||
)
|
||||
|
||||
# Verify both name and label resolve to full path
|
||||
interface = leaf_module.interfaces.first()
|
||||
self.assertEqual(interface.name, 'SFP 1/2')
|
||||
self.assertEqual(interface.label, 'LBL 1/2')
|
||||
|
||||
def test_non_interface_component_template_substitution(self):
|
||||
"""
|
||||
T5: Non-interface modular component templates (ConsolePortTemplate).
|
||||
Ensures the fix is general to all ModularComponentTemplateModel subclasses.
|
||||
"""
|
||||
site = Site.objects.create(name='T5 Site', slug='t5-site')
|
||||
manufacturer = Manufacturer.objects.create(name='T5 Manufacturer', slug='t5-manufacturer')
|
||||
device_role = DeviceRole.objects.create(name='T5 Role', slug='t5-role')
|
||||
|
||||
# Create device type with module bay
|
||||
device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='T5 Chassis',
|
||||
slug='t5-chassis'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
device_type=device_type,
|
||||
name='Bay 1',
|
||||
position='1'
|
||||
)
|
||||
|
||||
# Create line card module type with nested bay at position 2
|
||||
line_card_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='T5 Line Card'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
module_type=line_card_type,
|
||||
name='Nested Bay',
|
||||
position='2'
|
||||
)
|
||||
|
||||
# Create leaf module type with ConsolePortTemplate using single token
|
||||
leaf_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='T5 Leaf'
|
||||
)
|
||||
ConsolePortTemplate.objects.create(
|
||||
module_type=leaf_type,
|
||||
name='Console {module}',
|
||||
label='{module}'
|
||||
)
|
||||
|
||||
# Create device and install modules
|
||||
device = Device.objects.create(
|
||||
name='T5 Device',
|
||||
device_type=device_type,
|
||||
role=device_role,
|
||||
site=site
|
||||
)
|
||||
|
||||
bay1 = device.modulebays.get(name='Bay 1')
|
||||
line_card = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=bay1,
|
||||
module_type=line_card_type
|
||||
)
|
||||
|
||||
nested_bay = line_card.modulebays.get(name='Nested Bay')
|
||||
leaf_module = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=nested_bay,
|
||||
module_type=leaf_type
|
||||
)
|
||||
|
||||
# Verify ConsolePort resolves with full path
|
||||
console_port = leaf_module.consoleports.first()
|
||||
self.assertEqual(console_port.name, 'Console 1/2')
|
||||
self.assertEqual(console_port.label, '1/2')
|
||||
|
||||
def test_positions_with_slashes_join_correctly(self):
|
||||
"""
|
||||
T6: Positions that already contain slashes don't break joining (depth=2, single token).
|
||||
Some platforms use positions like 0/1 (PIC/port style) even before nesting.
|
||||
"""
|
||||
site = Site.objects.create(name='T6 Site', slug='t6-site')
|
||||
manufacturer = Manufacturer.objects.create(name='T6 Manufacturer', slug='t6-manufacturer')
|
||||
device_role = DeviceRole.objects.create(name='T6 Role', slug='t6-role')
|
||||
|
||||
# Create device type with module bay using slash in position
|
||||
device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='T6 Chassis',
|
||||
slug='t6-chassis'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
device_type=device_type,
|
||||
name='PIC Bay',
|
||||
position='0/1' # Position already contains slash
|
||||
)
|
||||
|
||||
# Create line card module type with nested bay at position 2
|
||||
line_card_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='T6 Line Card'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
module_type=line_card_type,
|
||||
name='Nested Bay',
|
||||
position='2'
|
||||
)
|
||||
|
||||
# Create leaf module type with single-token interface template
|
||||
leaf_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='T6 Leaf'
|
||||
)
|
||||
InterfaceTemplate.objects.create(
|
||||
module_type=leaf_type,
|
||||
name='Gi{module}',
|
||||
type=InterfaceTypeChoices.TYPE_1GE_FIXED
|
||||
)
|
||||
|
||||
# Create device and install modules
|
||||
device = Device.objects.create(
|
||||
name='T6 Device',
|
||||
device_type=device_type,
|
||||
role=device_role,
|
||||
site=site
|
||||
)
|
||||
|
||||
bay1 = device.modulebays.get(name='PIC Bay')
|
||||
line_card = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=bay1,
|
||||
module_type=line_card_type
|
||||
)
|
||||
|
||||
nested_bay = line_card.modulebays.get(name='Nested Bay')
|
||||
leaf_module = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=nested_bay,
|
||||
module_type=leaf_type
|
||||
)
|
||||
|
||||
# Verify: 0/1 + 2 = 0/1/2
|
||||
interface = leaf_module.interfaces.first()
|
||||
self.assertEqual(interface.name, 'Gi0/1/2')
|
||||
|
||||
def test_depth_1_single_token_no_extra_slashes(self):
|
||||
"""
|
||||
T7: Ensure depth=1 single-token still resolves to the position, not an unnecessary "path join".
|
||||
"""
|
||||
site = Site.objects.create(name='T7 Site', slug='t7-site')
|
||||
manufacturer = Manufacturer.objects.create(name='T7 Manufacturer', slug='t7-manufacturer')
|
||||
device_role = DeviceRole.objects.create(name='T7 Role', slug='t7-role')
|
||||
|
||||
# Create device type with module bay at position 7
|
||||
device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='T7 Chassis',
|
||||
slug='t7-chassis'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
device_type=device_type,
|
||||
name='Bay 7',
|
||||
position='7'
|
||||
)
|
||||
|
||||
# Create module type with single-token template
|
||||
module_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='T7 Module'
|
||||
)
|
||||
InterfaceTemplate.objects.create(
|
||||
module_type=module_type,
|
||||
name='{module}',
|
||||
type=InterfaceTypeChoices.TYPE_1GE_FIXED
|
||||
)
|
||||
|
||||
# Create device and install module directly at depth=1
|
||||
device = Device.objects.create(
|
||||
name='T7 Device',
|
||||
device_type=device_type,
|
||||
role=device_role,
|
||||
site=site
|
||||
)
|
||||
|
||||
bay = device.modulebays.get(name='Bay 7')
|
||||
module = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=bay,
|
||||
module_type=module_type
|
||||
)
|
||||
|
||||
# Verify: just "7", not "7/" or similar
|
||||
interface = module.interfaces.first()
|
||||
self.assertEqual(interface.name, '7')
|
||||
|
||||
def test_multi_occurrence_tokens_level_by_level(self):
|
||||
"""
|
||||
T8: Multiple occurrences of {module} in a single template (token_count > 1) still level-by-level.
|
||||
Ensure the token_count logic and replacement loop behaves with duplicated patterns.
|
||||
"""
|
||||
site = Site.objects.create(name='T8 Site', slug='t8-site')
|
||||
manufacturer = Manufacturer.objects.create(name='T8 Manufacturer', slug='t8-manufacturer')
|
||||
device_role = DeviceRole.objects.create(name='T8 Role', slug='t8-role')
|
||||
|
||||
# Create device type with module bay
|
||||
device_type = DeviceType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='T8 Chassis',
|
||||
slug='t8-chassis'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
device_type=device_type,
|
||||
name='Bay 1',
|
||||
position='1'
|
||||
)
|
||||
|
||||
# Create line card module type with nested bay at position 2
|
||||
line_card_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='T8 Line Card'
|
||||
)
|
||||
ModuleBayTemplate.objects.create(
|
||||
module_type=line_card_type,
|
||||
name='Nested Bay',
|
||||
position='2'
|
||||
)
|
||||
|
||||
# Create leaf module type with 2-token template (non-slash separator)
|
||||
leaf_type = ModuleType.objects.create(
|
||||
manufacturer=manufacturer,
|
||||
model='T8 Leaf'
|
||||
)
|
||||
InterfaceTemplate.objects.create(
|
||||
module_type=leaf_type,
|
||||
name='X{module}-Y{module}',
|
||||
type=InterfaceTypeChoices.TYPE_1GE_FIXED
|
||||
)
|
||||
|
||||
# Create device and install modules
|
||||
device = Device.objects.create(
|
||||
name='T8 Device',
|
||||
device_type=device_type,
|
||||
role=device_role,
|
||||
site=site
|
||||
)
|
||||
|
||||
bay1 = device.modulebays.get(name='Bay 1')
|
||||
line_card = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=bay1,
|
||||
module_type=line_card_type
|
||||
)
|
||||
|
||||
nested_bay = line_card.modulebays.get(name='Nested Bay')
|
||||
leaf_module = Module.objects.create(
|
||||
device=device,
|
||||
module_bay=nested_bay,
|
||||
module_type=leaf_type
|
||||
)
|
||||
|
||||
# Verify: X1-Y2 (level-by-level, not full-path stuffed into first)
|
||||
interface = leaf_module.interfaces.first()
|
||||
self.assertEqual(interface.name, 'X1-Y2')
|
||||
|
||||
|
||||
class CableTestCase(TestCase):
|
||||
|
||||
|
||||
@@ -129,6 +129,12 @@ class DeviceDimensionsPanel(panels.ObjectAttributesPanel):
|
||||
total_weight = attrs.TemplatedAttr('total_weight', template_name='dcim/device/attrs/total_weight.html')
|
||||
|
||||
|
||||
class DeviceRolePanel(panels.NestedGroupObjectPanel):
|
||||
color = attrs.ColorAttr('color')
|
||||
vm_role = attrs.BooleanAttr('vm_role', label=_('VM role'))
|
||||
config_template = attrs.RelatedObjectAttr('config_template', linkify=True)
|
||||
|
||||
|
||||
class DeviceTypePanel(panels.ObjectAttributesPanel):
|
||||
manufacturer = attrs.RelatedObjectAttr('manufacturer', linkify=True)
|
||||
model = attrs.TextAttr('model')
|
||||
@@ -145,11 +151,36 @@ class DeviceTypePanel(panels.ObjectAttributesPanel):
|
||||
rear_image = attrs.ImageAttr('rear_image')
|
||||
|
||||
|
||||
class ModulePanel(panels.ObjectAttributesPanel):
|
||||
device = attrs.RelatedObjectAttr('device', linkify=True)
|
||||
device_type = attrs.RelatedObjectAttr('device.device_type', linkify=True, grouped_by='manufacturer')
|
||||
module_bay = attrs.NestedObjectAttr('module_bay')
|
||||
status = attrs.ChoiceAttr('status')
|
||||
description = attrs.TextAttr('description')
|
||||
serial = attrs.TextAttr('serial', label=_('Serial number'), style='font-monospace', copy_button=True)
|
||||
asset_tag = attrs.TextAttr('asset_tag', style='font-monospace', copy_button=True)
|
||||
|
||||
|
||||
class ModuleTypeProfilePanel(panels.ObjectAttributesPanel):
|
||||
name = attrs.TextAttr('name')
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
class ModuleTypePanel(panels.ObjectAttributesPanel):
|
||||
profile = attrs.RelatedObjectAttr('profile', linkify=True)
|
||||
manufacturer = attrs.RelatedObjectAttr('manufacturer', linkify=True)
|
||||
model = attrs.TextAttr('name')
|
||||
part_number = attrs.TextAttr('part_number')
|
||||
description = attrs.TextAttr('description')
|
||||
airflow = attrs.ChoiceAttr('airflow')
|
||||
weight = attrs.NumericAttr('weight', unit_accessor='get_weight_unit_display')
|
||||
|
||||
|
||||
class PlatformPanel(panels.NestedGroupObjectPanel):
|
||||
manufacturer = attrs.RelatedObjectAttr('manufacturer', linkify=True)
|
||||
config_template = attrs.RelatedObjectAttr('config_template', linkify=True)
|
||||
|
||||
|
||||
class VirtualChassisMembersPanel(panels.ObjectPanel):
|
||||
"""
|
||||
A panel which lists all members of a virtual chassis.
|
||||
|
||||
@@ -21,8 +21,8 @@ from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
|
||||
from netbox.object_actions import *
|
||||
from netbox.ui import actions, layout
|
||||
from netbox.ui.panels import (
|
||||
CommentsPanel, JSONPanel, NestedGroupObjectPanel, ObjectsTablePanel, OrganizationalObjectPanel, RelatedObjectsPanel,
|
||||
TemplatePanel,
|
||||
CommentsPanel, JSONPanel, NestedGroupObjectPanel, ObjectsTablePanel, OrganizationalObjectPanel, Panel,
|
||||
RelatedObjectsPanel, TemplatePanel,
|
||||
)
|
||||
from netbox.views import generic
|
||||
from utilities.forms import ConfirmationForm
|
||||
@@ -1656,6 +1656,22 @@ class ModuleTypeListView(generic.ObjectListView):
|
||||
@register_model_view(ModuleType)
|
||||
class ModuleTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = ModuleType.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.ModuleTypePanel(),
|
||||
TagsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
Panel(
|
||||
title=_('Attributes'),
|
||||
template_name='dcim/panels/module_type_attributes.html',
|
||||
),
|
||||
RelatedObjectsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
ImageAttachmentsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
@@ -2294,6 +2310,27 @@ class DeviceRoleListView(generic.ObjectListView):
|
||||
@register_model_view(DeviceRole)
|
||||
class DeviceRoleView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = DeviceRole.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.DeviceRolePanel(),
|
||||
TagsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ObjectsTablePanel(
|
||||
model='dcim.DeviceRole',
|
||||
title=_('Child Device Roles'),
|
||||
filters={'parent_id': lambda ctx: ctx['object'].pk},
|
||||
actions=[
|
||||
actions.AddObject('dcim.DeviceRole', url_params={'parent': lambda ctx: ctx['object'].pk}),
|
||||
],
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
@@ -2373,6 +2410,27 @@ class PlatformListView(generic.ObjectListView):
|
||||
@register_model_view(Platform)
|
||||
class PlatformView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = Platform.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.PlatformPanel(),
|
||||
TagsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ObjectsTablePanel(
|
||||
model='dcim.Platform',
|
||||
title=_('Child Platforms'),
|
||||
filters={'parent_id': lambda ctx: ctx['object'].pk},
|
||||
actions=[
|
||||
actions.AddObject('dcim.Platform', url_params={'parent': lambda ctx: ctx['object'].pk}),
|
||||
],
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
@@ -2763,6 +2821,21 @@ class ModuleListView(generic.ObjectListView):
|
||||
@register_model_view(Module)
|
||||
class ModuleView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = Module.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.ModulePanel(),
|
||||
TagsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
Panel(
|
||||
title=_('Module Type'),
|
||||
template_name='dcim/panels/module_type.html',
|
||||
),
|
||||
RelatedObjectsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
|
||||
@@ -43,15 +43,18 @@ class Panel:
|
||||
Parameters:
|
||||
title (str): The human-friendly title of the panel
|
||||
actions (list): An iterable of PanelActions to include in the panel header
|
||||
template_name (str): Overrides the default template name, if defined
|
||||
"""
|
||||
template_name = None
|
||||
title = None
|
||||
actions = None
|
||||
|
||||
def __init__(self, title=None, actions=None):
|
||||
def __init__(self, title=None, actions=None, template_name=None):
|
||||
if title is not None:
|
||||
self.title = title
|
||||
self.actions = actions or self.actions or []
|
||||
if template_name is not None:
|
||||
self.template_name = template_name
|
||||
|
||||
def get_context(self, context):
|
||||
"""
|
||||
@@ -316,9 +319,8 @@ class TemplatePanel(Panel):
|
||||
Parameters:
|
||||
template_name (str): The name of the template to render
|
||||
"""
|
||||
def __init__(self, template_name, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.template_name = template_name
|
||||
def __init__(self, template_name):
|
||||
super().__init__(template_name=template_name)
|
||||
|
||||
def render(self, context):
|
||||
# Pass the entire context to the template
|
||||
|
||||
@@ -15,67 +15,3 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock extra_controls %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Device Role" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Parent" %}</th>
|
||||
<td>{{ object.parent|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Color" %}</th>
|
||||
<td>
|
||||
<span class="badge color-label" style="background-color: #{{ object.color }}"> </span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "VM Role" %}</th>
|
||||
<td>{% checkmark object.vm_role %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Config Template" %}</th>
|
||||
<td>{{ object.config_template|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Child Device Roles" %}
|
||||
{% if perms.dcim.add_devicerole %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'dcim:devicerole_add' %}?parent={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Device Role" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'dcim:devicerole_list' parent_id=object.pk %}
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% block contentx %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{{ object.manufacturer }} {{ object.model }}{% endblock %}
|
||||
@@ -14,92 +11,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_controls %}
|
||||
{% include 'dcim/inc/moduletype_buttons.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Module Type" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Profile" %}</th>
|
||||
<td>{{ object.profile|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Manufacturer" %}</th>
|
||||
<td>{{ object.manufacturer|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Model Name" %}</th>
|
||||
<td>{{ object.model }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Part Number" %}</th>
|
||||
<td>{{ object.part_number|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Airflow" %}</th>
|
||||
<td>{{ object.get_airflow_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Weight" %}</th>
|
||||
<td>
|
||||
{% if object.weight %}
|
||||
{{ object.weight|floatformat }} {{ object.get_weight_unit_display }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Attributes" %}</h2>
|
||||
{% if not object.profile %}
|
||||
<div class="card-body text-muted">
|
||||
{% trans "No profile assigned" %}
|
||||
</div>
|
||||
{% elif object.attributes %}
|
||||
<table class="table table-hover attr-table">
|
||||
{% for k, v in object.attributes.items %}
|
||||
<tr>
|
||||
<th scope="row">{{ k }}</th>
|
||||
<td>
|
||||
{% if v is True or v is False %}
|
||||
{% checkmark v %}
|
||||
{% else %}
|
||||
{{ v|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="card-body text-muted">
|
||||
{% trans "None" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/image_attachments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'dcim/inc/moduletype_buttons.html' %}
|
||||
{% endblock %}
|
||||
|
||||
27
netbox/templates/dcim/panels/module_type.html
Normal file
27
netbox/templates/dcim/panels/module_type.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{% extends "ui/panels/_base.html" %}
|
||||
{% load helpers i18n %}
|
||||
|
||||
{% block panel_content %}
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Manufacturer" %}</th>
|
||||
<td>{{ object.module_type.manufacturer|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Model" %}</th>
|
||||
<td>{{ object.module_type|linkify }}</td>
|
||||
</tr>
|
||||
{% for k, v in object.module_type.attributes.items %}
|
||||
<tr>
|
||||
<th scope="row">{{ k }}</th>
|
||||
<td>
|
||||
{% if v is True or v is False %}
|
||||
{% checkmark v %}
|
||||
{% else %}
|
||||
{{ v|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock panel_content %}
|
||||
29
netbox/templates/dcim/panels/module_type_attributes.html
Normal file
29
netbox/templates/dcim/panels/module_type_attributes.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{% extends "ui/panels/_base.html" %}
|
||||
{% load helpers i18n %}
|
||||
|
||||
{% block panel_content %}
|
||||
{% if not object.profile %}
|
||||
<div class="card-body text-muted">
|
||||
{% trans "No profile assigned" %}
|
||||
</div>
|
||||
{% elif object.attributes %}
|
||||
<table class="table table-hover attr-table">
|
||||
{% for k, v in object.attributes.items %}
|
||||
<tr>
|
||||
<th scope="row">{{ k }}</th>
|
||||
<td>
|
||||
{% if v is True or v is False %}
|
||||
{% checkmark v %}
|
||||
{% else %}
|
||||
{{ v|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="card-body text-muted">
|
||||
{% trans "None" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock panel_content %}
|
||||
@@ -18,61 +18,3 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock extra_controls %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Platform" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Parent" %}</th>
|
||||
<td>{{ object.parent|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Manufacturer" %}</th>
|
||||
<td>{{ object.manufacturer|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Config Template" %}</th>
|
||||
<td>{{ object.config_template|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Child Platforms" %}
|
||||
{% if perms.dcim.add_platform %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'dcim:platform_add' %}?parent={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Platform" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'dcim:platform_list' parent_id=object.pk %}
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user