Skip to content

Commit b9f8370

Browse files
Fixes #11156 - Allow InventoryItem component reassignment (#11256)
* Allow re-assigning InventoryItem components * Refactor logic for finding initial component assignment on InventoryItems * PEP8 fix * Fix wrong HTML causing tab list to extend past the end of the parent row * Tweak form field labels Co-authored-by: jeremystretch <[email protected]>
1 parent 1c636ea commit b9f8370

File tree

4 files changed

+212
-23
lines changed

4 files changed

+212
-23
lines changed

netbox/dcim/forms/model_forms.py

Lines changed: 99 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1549,38 +1549,125 @@ class InventoryItemForm(DeviceComponentForm):
15491549
queryset=Manufacturer.objects.all(),
15501550
required=False
15511551
)
1552-
component_type = ContentTypeChoiceField(
1553-
queryset=ContentType.objects.all(),
1554-
limit_choices_to=MODULAR_COMPONENT_MODELS,
1552+
1553+
# Assigned component selectors
1554+
consoleport = DynamicModelChoiceField(
1555+
queryset=ConsolePort.objects.all(),
15551556
required=False,
1556-
widget=forms.HiddenInput
1557+
query_params={
1558+
'device_id': '$device'
1559+
},
1560+
label=_('Console port')
15571561
)
1558-
component_id = forms.IntegerField(
1562+
consoleserverport = DynamicModelChoiceField(
1563+
queryset=ConsoleServerPort.objects.all(),
15591564
required=False,
1560-
widget=forms.HiddenInput
1565+
query_params={
1566+
'device_id': '$device'
1567+
},
1568+
label=_('Console server port')
1569+
)
1570+
frontport = DynamicModelChoiceField(
1571+
queryset=FrontPort.objects.all(),
1572+
required=False,
1573+
query_params={
1574+
'device_id': '$device'
1575+
},
1576+
label=_('Front port')
1577+
)
1578+
interface = DynamicModelChoiceField(
1579+
queryset=Interface.objects.all(),
1580+
required=False,
1581+
query_params={
1582+
'device_id': '$device'
1583+
},
1584+
label=_('Interface')
1585+
)
1586+
poweroutlet = DynamicModelChoiceField(
1587+
queryset=PowerOutlet.objects.all(),
1588+
required=False,
1589+
query_params={
1590+
'device_id': '$device'
1591+
},
1592+
label=_('Power outlet')
1593+
)
1594+
powerport = DynamicModelChoiceField(
1595+
queryset=PowerPort.objects.all(),
1596+
required=False,
1597+
query_params={
1598+
'device_id': '$device'
1599+
},
1600+
label=_('Power port')
1601+
)
1602+
rearport = DynamicModelChoiceField(
1603+
queryset=RearPort.objects.all(),
1604+
required=False,
1605+
query_params={
1606+
'device_id': '$device'
1607+
},
1608+
label=_('Rear port')
15611609
)
15621610

15631611
fieldsets = (
15641612
('Inventory Item', ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')),
15651613
('Hardware', ('manufacturer', 'part_id', 'serial', 'asset_tag')),
15661614
)
15671615

1616+
class Meta:
1617+
model = InventoryItem
1618+
fields = [
1619+
'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
1620+
'description', 'tags',
1621+
]
1622+
15681623
def __init__(self, *args, **kwargs):
1624+
instance = kwargs.get('instance')
1625+
initial = kwargs.get('initial', {}).copy()
1626+
component_type = initial.get('component_type')
1627+
component_id = initial.get('component_id')
1628+
1629+
# Used for picking the default active tab for component selection
1630+
self.no_component = True
1631+
1632+
if instance:
1633+
# When editing set the initial value for component selectin
1634+
for component_model in ContentType.objects.filter(MODULAR_COMPONENT_MODELS):
1635+
if type(instance.component) is component_model.model_class():
1636+
initial[component_model.model] = instance.component
1637+
self.no_component = False
1638+
break
1639+
elif component_type and component_id:
1640+
# When adding the InventoryItem from a component page
1641+
if content_type := ContentType.objects.filter(MODULAR_COMPONENT_MODELS).filter(pk=component_type).first():
1642+
if component := content_type.model_class().objects.filter(pk=component_id).first():
1643+
initial[content_type.model] = component
1644+
self.no_component = False
1645+
1646+
kwargs['initial'] = initial
1647+
15691648
super().__init__(*args, **kwargs)
15701649

15711650
# Specifically allow editing the device of IntentoryItems
15721651
if self.instance.pk:
15731652
self.fields['device'].disabled = False
15741653

1575-
class Meta:
1576-
model = InventoryItem
1577-
fields = [
1578-
'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
1579-
'description', 'component_type', 'component_id', 'tags',
1654+
def clean(self):
1655+
super().clean()
1656+
1657+
# Handle object assignment
1658+
selected_objects = [
1659+
field for field in (
1660+
'consoleport', 'consoleserverport', 'frontport', 'interface', 'poweroutlet', 'powerport', 'rearport'
1661+
) if self.cleaned_data[field]
15801662
]
1663+
if len(selected_objects) > 1:
1664+
raise forms.ValidationError("An InventoryItem can only be assigned to a single component.")
1665+
elif selected_objects:
1666+
self.instance.component = self.cleaned_data[selected_objects[0]]
1667+
else:
1668+
self.instance.component = None
15811669

15821670

1583-
#
15841671
# Device component roles
15851672
#
15861673

netbox/dcim/models/device_components.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1146,3 +1146,8 @@ def clean(self):
11461146
# When moving an InventoryItem to another device, remove any associated component
11471147
if self.component and self.component.device != self.device:
11481148
self.component = None
1149+
else:
1150+
if self.component and self.component.device != self.device:
1151+
raise ValidationError({
1152+
"device": "Cannot assign inventory item to component on another device"
1153+
})

netbox/dcim/views.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2914,23 +2914,14 @@ class InventoryItemView(generic.ObjectView):
29142914
class InventoryItemEditView(generic.ObjectEditView):
29152915
queryset = InventoryItem.objects.all()
29162916
form = forms.InventoryItemForm
2917+
template_name = 'dcim/inventoryitem_edit.html'
29172918

29182919

29192920
class InventoryItemCreateView(generic.ComponentCreateView):
29202921
queryset = InventoryItem.objects.all()
29212922
form = forms.InventoryItemCreateForm
29222923
model_form = forms.InventoryItemForm
2923-
2924-
def alter_object(self, instance, request):
2925-
# Set component (if any)
2926-
component_type = request.GET.get('component_type')
2927-
component_id = request.GET.get('component_id')
2928-
2929-
if component_type and component_id:
2930-
content_type = get_object_or_404(ContentType, pk=component_type)
2931-
instance.component = get_object_or_404(content_type.model_class(), pk=component_id)
2932-
2933-
return instance
2924+
template_name = 'dcim/inventoryitem_edit.html'
29342925

29352926

29362927
@register_model_view(InventoryItem, 'delete')
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
{% extends 'generic/object_edit.html' %}
2+
{% load static %}
3+
{% load form_helpers %}
4+
{% load helpers %}
5+
6+
{% block form %}
7+
<div class="field-group my-5">
8+
<div class="row mb-2">
9+
<h5 class="offset-sm-3">InventoryItem</h5>
10+
</div>
11+
{% render_field form.device %}
12+
{% render_field form.parent %}
13+
{% render_field form.name %}
14+
{% render_field form.label %}
15+
{% render_field form.role %}
16+
{% render_field form.description %}
17+
{% render_field form.tags %}
18+
</div>
19+
20+
<div class="field-group my-5">
21+
<div class="row mb-2">
22+
<h5 class="offset-sm-3">Hardware</h5>
23+
</div>
24+
{% render_field form.manufacturer %}
25+
{% render_field form.part_id %}
26+
{% render_field form.serial %}
27+
{% render_field form.asset_tag %}
28+
</div>
29+
30+
<div class="field-group my-5">
31+
<div class="row mb-2">
32+
<h5 class="offset-sm-3">Component Assignment</h5>
33+
</div>
34+
<div class="row mb-2 offset-sm-3">
35+
<ul class="nav nav-pills" role="tablist">
36+
<li role="presentation" class="nav-item">
37+
<button role="tab" type="button" id="consoleport_tab" data-bs-toggle="tab" aria-controls="consoleport" data-bs-target="#consoleport" class="nav-link {% if form.initial.consoleport or form.no_component %}active{% endif %}">
38+
Console Port
39+
</button>
40+
</li>
41+
<li role="presentation" class="nav-item">
42+
<button role="tab" type="button" id="consoleserverport_tab" data-bs-toggle="tab" aria-controls="consoleserverport" data-bs-target="#consoleserverport" class="nav-link {% if form.initial.consoleserverport %}active{% endif %}">
43+
Console Server Port
44+
</button>
45+
</li>
46+
<li role="presentation" class="nav-item">
47+
<button role="tab" type="button" id="frontport_tab" data-bs-toggle="tab" aria-controls="frontport" data-bs-target="#frontport" class="nav-link {% if form.initial.frontport %}active{% endif %}">
48+
Front Port
49+
</button>
50+
</li>
51+
<li role="presentation" class="nav-item">
52+
<button role="tab" type="button" id="interface_tab" data-bs-toggle="tab" aria-controls="interface" data-bs-target="#interface" class="nav-link {% if form.initial.interface %}active{% endif %}">
53+
Interface
54+
</button>
55+
</li>
56+
<li role="presentation" class="nav-item">
57+
<button role="tab" type="button" id="poweroutlet_tab" data-bs-toggle="tab" aria-controls="poweroutlet" data-bs-target="#poweroutlet" class="nav-link {% if form.initial.poweroutlet %}active{% endif %}">
58+
Power Outlet
59+
</button>
60+
</li>
61+
<li role="presentation" class="nav-item">
62+
<button role="tab" type="button" id="powerport_tab" data-bs-toggle="tab" aria-controls="powerport" data-bs-target="#powerport" class="nav-link {% if form.initial.powerport %}active{% endif %}">
63+
Power Port
64+
</button>
65+
</li>
66+
<li role="presentation" class="nav-item">
67+
<button role="tab" type="button" id="rearport_tab" data-bs-toggle="tab" aria-controls="rearport" data-bs-target="#rearport" class="nav-link {% if form.initial.rearport %}active{% endif %}">
68+
Rear Port
69+
</button>
70+
</li>
71+
</ul>
72+
</div>
73+
<div class="tab-content p-0 border-0">
74+
<div class="tab-pane {% if form.initial.consoleport or form.no_component %}active{% endif %}" id="consoleport" role="tabpanel" aria-labeled-by="consoleport_tab">
75+
{% render_field form.consoleport %}
76+
</div>
77+
<div class="tab-pane {% if form.initial.consoleserverport %}active{% endif %}" id="consoleserverport" role="tabpanel" aria-labeled-by="consoleserverport_tab">
78+
{% render_field form.consoleserverport %}
79+
</div>
80+
<div class="tab-pane {% if form.initial.frontport %}active{% endif %}" id="frontport" role="tabpanel" aria-labeled-by="frontport_tab">
81+
{% render_field form.frontport %}
82+
</div>
83+
<div class="tab-pane {% if form.initial.interface %}active{% endif %}" id="interface" role="tabpanel" aria-labeled-by="interface_tab">
84+
{% render_field form.interface %}
85+
</div>
86+
<div class="tab-pane {% if form.initial.poweroutlet %}active{% endif %}" id="poweroutlet" role="tabpanel" aria-labeled-by="poweroutlet_tab">
87+
{% render_field form.poweroutlet %}
88+
</div>
89+
<div class="tab-pane {% if form.initial.powerport %}active{% endif %}" id="powerport" role="tabpanel" aria-labeled-by="powerport_tab">
90+
{% render_field form.powerport %}
91+
</div>
92+
<div class="tab-pane {% if form.initial.rearport %}active{% endif %}" id="rearport" role="tabpanel" aria-labeled-by="rearport_tab">
93+
{% render_field form.rearport %}
94+
</div>
95+
</div>
96+
</div>
97+
98+
{% if form.custom_fields %}
99+
<div class="field-group my-5">
100+
<div class="row mb-2">
101+
<h5 class="offset-sm-3">Custom Fields</h5>
102+
</div>
103+
{% render_custom_fields form %}
104+
</div>
105+
{% endif %}
106+
{% endblock %}

0 commit comments

Comments
 (0)