Sfoglia il codice sorgente

Enable tabbed group fields in fieldsets

Jeremy Stretch 1 anno fa
parent
commit
4c7b6fcec0

+ 15 - 9
netbox/dcim/forms/model_forms.py

@@ -16,7 +16,7 @@ from utilities.forms.fields import (
     CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField,
     NumericArrayField, SlugField,
 )
-from utilities.forms.rendering import InlineFields
+from utilities.forms.rendering import InlineFields, TabbedFieldGroups
 from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
 from virtualization.models import Cluster
 from wireless.models import WirelessLAN, WirelessLANGroup
@@ -237,8 +237,8 @@ class RackForm(TenancyForm, NetBoxModelForm):
             'width',
             'starting_unit',
             'u_height',
-            InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')),
-            InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')),
+            InlineFields(_('Outer Dimensions'), 'outer_width', 'outer_depth', 'outer_unit'),
+            InlineFields(_('Weight'), 'weight', 'max_weight', 'weight_unit'),
             'mounting_depth',
             'desc_units',
         )),
@@ -1414,6 +1414,17 @@ class InventoryItemForm(DeviceComponentForm):
     fieldsets = (
         (_('Inventory Item'), ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')),
         (_('Hardware'), ('manufacturer', 'part_id', 'serial', 'asset_tag')),
+        (_('Component Assignment'), (
+            TabbedFieldGroups(
+                (_('Interface'), 'interface'),
+                (_('Console Port'), 'consoleport'),
+                (_('Console Server Port'), 'consoleserverport'),
+                (_('Front Port'), 'frontport'),
+                (_('Rear Port'), 'rearport'),
+                (_('Power Port'), 'powerport'),
+                (_('Power Outlet'), 'poweroutlet'),
+            ),
+        ))
     )
 
     class Meta:
@@ -1429,22 +1440,17 @@ class InventoryItemForm(DeviceComponentForm):
         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
+            # When editing set the initial value for component selection
             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
 

+ 0 - 2
netbox/dcim/views.py

@@ -2924,14 +2924,12 @@ 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
-    template_name = 'dcim/inventoryitem_edit.html'
 
 
 @register_model_view(InventoryItem, 'delete')

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

@@ -1,107 +0,0 @@
-{% extends 'generic/object_edit.html' %}
-{% load static %}
-{% load form_helpers %}
-{% load helpers %}
-{% load i18n %}
-
-{% block form %}
-    <div class="field-group my-5">
-      <div class="row">
-        <h5 class="col-9 offset-3">{% trans "Inventory Item" %}</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">
-        <h5 class="col-9 offset-3">{% trans "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">
-        <h5 class="col-9 offset-3">{% trans "Component Assignment" %}</h5>
-      </div>
-      <div class="row offset-sm-3">
-        <ul class="nav nav-pills mb-1" 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 %}">
-                {% trans "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 %}">
-                {% trans "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 %}">
-                {% trans "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 %}">
-                {% trans "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 %}">
-                {% trans "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 %}">
-                {% trans "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 %}">
-                {% trans "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">
-          <h5 class="col-9 offset-3">{% trans "Custom Fields" %}</h5>
-        </div>
-        {% render_custom_fields form %}
-      </div>
-    {% endif %}
-{% endblock %}

+ 35 - 2
netbox/utilities/forms/rendering.py

@@ -1,10 +1,43 @@
+import random
+import string
+from functools import cached_property
+
 __all__ = (
+    'FieldGroup',
     'InlineFields',
+    'TabbedFieldGroups',
 )
 
 
-class InlineFields:
+class FieldGroup:
 
-    def __init__(self, *field_names, label=None):
+    def __init__(self, label, *field_names):
         self.field_names = field_names
         self.label = label
+
+
+class InlineFields(FieldGroup):
+    pass
+
+
+class TabbedFieldGroups:
+
+    def __init__(self, *groups):
+        self.groups = [
+            FieldGroup(*group) for group in groups
+        ]
+
+        # Initialize a random ID for the group (for tab selection)
+        self.id = ''.join(
+            random.choice(string.ascii_lowercase + string.digits) for _ in range(8)
+        )
+
+    @cached_property
+    def tabs(self):
+        return [
+            {
+                'id': f'{self.id}_{i}',
+                'title': group.label,
+                'fields': group.field_names,
+            } for i, group in enumerate(self.groups, start=1)
+        ]

+ 26 - 0
netbox/utilities/templates/form_helpers/render_fieldset.html

@@ -7,9 +7,11 @@
     </div>
   {% endif %}
   {% for layout, title, items in rows %}
+
     {% if layout == 'field' %}
       {# Single form field #}
       {% render_field items.0 %}
+
     {% elif layout == 'inline' %}
       {# Multiple form fields on the same line #}
       <div class="row mb-3">
@@ -21,6 +23,30 @@
           </div>
         {% endfor %}
       </div>
+
+    {% elif layout == 'tabs' %}
+      {# Tabbed groups of fields #}
+      <div class="row offset-sm-3">
+        <ul class="nav nav-pills mb-1" role="tablist">
+          {% for tab in items %}
+            <li role="presentation" class="nav-item">
+              <button role="tab" type="button" id="{{ tab.id }}_tab" data-bs-toggle="tab" aria-controls="{{ tab.id }}" data-bs-target="#{{ tab.id }}" class="nav-link {% if tab.active %}active{% endif %}">
+                {% trans tab.title %}
+              </button>
+            </li>
+          {% endfor %}
+        </ul>
+      </div>
+      <div class="tab-content p-0 border-0">
+        {% for tab in items %}
+          <div class="tab-pane {% if tab.active %}active{% endif %}" id="{{ tab.id }}" role="tabpanel" aria-labeled-by="{{ tab.id }}_tab">
+            {% for field in tab.fields %}
+              {% render_field field %}
+            {% endfor %}
+          </div>
+        {% endfor %}
+      </div>
+
     {% endif %}
   {% endfor %}
 </div>

+ 16 - 1
netbox/utilities/templatetags/form_helpers.py

@@ -1,6 +1,6 @@
 from django import template
 
-from utilities.forms.rendering import InlineFields
+from utilities.forms.rendering import InlineFields, TabbedFieldGroups
 
 __all__ = (
     'getfield',
@@ -58,6 +58,21 @@ def render_fieldset(form, fieldset, heading=None):
             rows.append(
                 ('inline', item.label, [form[name] for name in item.field_names])
             )
+        elif type(item) is TabbedFieldGroups:
+            tabs = [
+                {
+                    'id': tab['id'],
+                    'title': tab['title'],
+                    'active': bool(form.initial.get(tab['fields'][0], False)),
+                    'fields': [form[name] for name in tab['fields']]
+                } for tab in item.tabs
+            ]
+            # If none of the tabs has been marked as active, activate the first one
+            if not any(tab['active'] for tab in tabs):
+                tabs[0]['active'] = True
+            rows.append(
+                ('tabs', None, tabs)
+            )
         else:
             rows.append(
                 ('field', None, [form[item]])