Quellcode durchsuchen

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 <jstretch@ns1.com>
kkthxbye vor 3 Jahren
Ursprung
Commit
b9f8370097

+ 99 - 12
netbox/dcim/forms/model_forms.py

@@ -1549,15 +1549,63 @@ class InventoryItemForm(DeviceComponentForm):
         queryset=Manufacturer.objects.all(),
         required=False
     )
-    component_type = ContentTypeChoiceField(
-        queryset=ContentType.objects.all(),
-        limit_choices_to=MODULAR_COMPONENT_MODELS,
+
+    # Assigned component selectors
+    consoleport = DynamicModelChoiceField(
+        queryset=ConsolePort.objects.all(),
         required=False,
-        widget=forms.HiddenInput
+        query_params={
+            'device_id': '$device'
+        },
+        label=_('Console port')
     )
-    component_id = forms.IntegerField(
+    consoleserverport = DynamicModelChoiceField(
+        queryset=ConsoleServerPort.objects.all(),
         required=False,
-        widget=forms.HiddenInput
+        query_params={
+            'device_id': '$device'
+        },
+        label=_('Console server port')
+    )
+    frontport = DynamicModelChoiceField(
+        queryset=FrontPort.objects.all(),
+        required=False,
+        query_params={
+            'device_id': '$device'
+        },
+        label=_('Front port')
+    )
+    interface = DynamicModelChoiceField(
+        queryset=Interface.objects.all(),
+        required=False,
+        query_params={
+            'device_id': '$device'
+        },
+        label=_('Interface')
+    )
+    poweroutlet = DynamicModelChoiceField(
+        queryset=PowerOutlet.objects.all(),
+        required=False,
+        query_params={
+            'device_id': '$device'
+        },
+        label=_('Power outlet')
+    )
+    powerport = DynamicModelChoiceField(
+        queryset=PowerPort.objects.all(),
+        required=False,
+        query_params={
+            'device_id': '$device'
+        },
+        label=_('Power port')
+    )
+    rearport = DynamicModelChoiceField(
+        queryset=RearPort.objects.all(),
+        required=False,
+        query_params={
+            'device_id': '$device'
+        },
+        label=_('Rear port')
     )
 
     fieldsets = (
@@ -1565,22 +1613,61 @@ class InventoryItemForm(DeviceComponentForm):
         ('Hardware', ('manufacturer', 'part_id', 'serial', 'asset_tag')),
     )
 
+    class Meta:
+        model = InventoryItem
+        fields = [
+            'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
+            'description', 'tags',
+        ]
+
     def __init__(self, *args, **kwargs):
+        instance = kwargs.get('instance')
+        initial = kwargs.get('initial', {}).copy()
+        component_type = initial.get('component_type')
+        component_id = initial.get('component_id')
+
+        # Used for picking the default active tab for component selection
+        self.no_component = True
+
+        if instance:
+            # When editing set the initial value for component selectin
+            for component_model in ContentType.objects.filter(MODULAR_COMPONENT_MODELS):
+                if type(instance.component) is component_model.model_class():
+                    initial[component_model.model] = instance.component
+                    self.no_component = False
+                    break
+        elif component_type and component_id:
+            # When adding the InventoryItem from a component page
+            if content_type := ContentType.objects.filter(MODULAR_COMPONENT_MODELS).filter(pk=component_type).first():
+                if component := content_type.model_class().objects.filter(pk=component_id).first():
+                    initial[content_type.model] = component
+                    self.no_component = False
+
+        kwargs['initial'] = initial
+
         super().__init__(*args, **kwargs)
 
         # Specifically allow editing the device of IntentoryItems
         if self.instance.pk:
             self.fields['device'].disabled = False
 
-    class Meta:
-        model = InventoryItem
-        fields = [
-            'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag',
-            'description', 'component_type', 'component_id', 'tags',
+    def clean(self):
+        super().clean()
+
+        # Handle object assignment
+        selected_objects = [
+            field for field in (
+                'consoleport', 'consoleserverport', 'frontport', 'interface', 'poweroutlet', 'powerport', 'rearport'
+            ) if self.cleaned_data[field]
         ]
+        if len(selected_objects) > 1:
+            raise forms.ValidationError("An InventoryItem can only be assigned to a single component.")
+        elif selected_objects:
+            self.instance.component = self.cleaned_data[selected_objects[0]]
+        else:
+            self.instance.component = None
 
 
-#
 # Device component roles
 #
 

+ 5 - 0
netbox/dcim/models/device_components.py

@@ -1146,3 +1146,8 @@ class InventoryItem(MPTTModel, ComponentModel):
             # When moving an InventoryItem to another device, remove any associated component
             if self.component and self.component.device != self.device:
                 self.component = None
+        else:
+            if self.component and self.component.device != self.device:
+                raise ValidationError({
+                    "device": "Cannot assign inventory item to component on another device"
+                })

+ 2 - 11
netbox/dcim/views.py

@@ -2914,23 +2914,14 @@ class InventoryItemView(generic.ObjectView):
 class InventoryItemEditView(generic.ObjectEditView):
     queryset = InventoryItem.objects.all()
     form = forms.InventoryItemForm
+    template_name = 'dcim/inventoryitem_edit.html'
 
 
 class InventoryItemCreateView(generic.ComponentCreateView):
     queryset = InventoryItem.objects.all()
     form = forms.InventoryItemCreateForm
     model_form = forms.InventoryItemForm
-
-    def alter_object(self, instance, request):
-        # Set component (if any)
-        component_type = request.GET.get('component_type')
-        component_id = request.GET.get('component_id')
-
-        if component_type and component_id:
-            content_type = get_object_or_404(ContentType, pk=component_type)
-            instance.component = get_object_or_404(content_type.model_class(), pk=component_id)
-
-        return instance
+    template_name = 'dcim/inventoryitem_edit.html'
 
 
 @register_model_view(InventoryItem, 'delete')

+ 106 - 0
netbox/templates/dcim/inventoryitem_edit.html

@@ -0,0 +1,106 @@
+{% extends 'generic/object_edit.html' %}
+{% load static %}
+{% load form_helpers %}
+{% load helpers %}
+
+{% block form %}
+    <div class="field-group my-5">
+      <div class="row mb-2">
+        <h5 class="offset-sm-3">InventoryItem</h5>
+      </div>
+      {% render_field form.device %}
+      {% render_field form.parent %}
+      {% render_field form.name %}
+      {% render_field form.label %}
+      {% render_field form.role %}
+      {% render_field form.description %}
+      {% render_field form.tags %}
+    </div>
+
+    <div class="field-group my-5">
+      <div class="row mb-2">
+        <h5 class="offset-sm-3">Hardware</h5>
+      </div>
+      {% render_field form.manufacturer %}
+      {% render_field form.part_id %}
+      {% render_field form.serial %}
+      {% render_field form.asset_tag %}
+    </div>
+
+    <div class="field-group my-5">
+      <div class="row mb-2">
+        <h5 class="offset-sm-3">Component Assignment</h5>
+      </div>
+      <div class="row mb-2 offset-sm-3">
+        <ul class="nav nav-pills" role="tablist">
+          <li role="presentation" class="nav-item">
+              <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 %}">
+                Console Port
+              </button>
+            </li>
+            <li role="presentation" class="nav-item">
+              <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 %}">
+                Console Server Port
+              </button>
+            </li>
+            <li role="presentation" class="nav-item">
+              <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 %}">
+                Front Port
+              </button>
+            </li>
+            <li role="presentation" class="nav-item">
+              <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 %}">
+                Interface
+              </button>
+            </li>
+            <li role="presentation" class="nav-item">
+              <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 %}">
+                Power Outlet
+              </button>
+            </li>
+            <li role="presentation" class="nav-item">
+              <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 %}">
+                Power Port
+              </button>
+            </li>
+            <li role="presentation" class="nav-item">
+              <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 %}">
+                Rear Port
+              </button>
+            </li>
+        </ul>
+      </div>
+      <div class="tab-content p-0 border-0">
+        <div class="tab-pane {% if form.initial.consoleport or form.no_component %}active{% endif %}" id="consoleport" role="tabpanel" aria-labeled-by="consoleport_tab">
+            {% render_field form.consoleport %}
+          </div>
+          <div class="tab-pane {% if form.initial.consoleserverport %}active{% endif %}" id="consoleserverport" role="tabpanel" aria-labeled-by="consoleserverport_tab">
+            {% render_field form.consoleserverport %}
+          </div>
+          <div class="tab-pane {% if form.initial.frontport %}active{% endif %}" id="frontport" role="tabpanel" aria-labeled-by="frontport_tab">
+            {% render_field form.frontport %}
+          </div>
+          <div class="tab-pane {% if form.initial.interface %}active{% endif %}" id="interface" role="tabpanel" aria-labeled-by="interface_tab">
+            {% render_field form.interface %}
+          </div>
+          <div class="tab-pane {% if form.initial.poweroutlet %}active{% endif %}" id="poweroutlet" role="tabpanel" aria-labeled-by="poweroutlet_tab">
+            {% render_field form.poweroutlet %}
+          </div>
+          <div class="tab-pane {% if form.initial.powerport %}active{% endif %}" id="powerport" role="tabpanel" aria-labeled-by="powerport_tab">
+            {% render_field form.powerport %}
+          </div>
+          <div class="tab-pane {% if form.initial.rearport %}active{% endif %}" id="rearport" role="tabpanel" aria-labeled-by="rearport_tab">
+            {% render_field form.rearport %}
+          </div>
+      </div>
+    </div>
+
+    {% if form.custom_fields %}
+      <div class="field-group my-5">
+        <div class="row mb-2">
+          <h5 class="offset-sm-3">Custom Fields</h5>
+        </div>
+        {% render_custom_fields form %}
+      </div>
+    {% endif %}
+{% endblock %}