Explorar o código

Enable tabbed group fields in fieldsets

Jeremy Stretch hai 1 ano
pai
achega
4c7b6fcec0

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

@@ -16,7 +16,7 @@ from utilities.forms.fields import (
     CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField,
     CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField,
     NumericArrayField, SlugField,
     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 utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
 from virtualization.models import Cluster
 from virtualization.models import Cluster
 from wireless.models import WirelessLAN, WirelessLANGroup
 from wireless.models import WirelessLAN, WirelessLANGroup
@@ -237,8 +237,8 @@ class RackForm(TenancyForm, NetBoxModelForm):
             'width',
             'width',
             'starting_unit',
             'starting_unit',
             'u_height',
             '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',
             'mounting_depth',
             'desc_units',
             'desc_units',
         )),
         )),
@@ -1414,6 +1414,17 @@ class InventoryItemForm(DeviceComponentForm):
     fieldsets = (
     fieldsets = (
         (_('Inventory Item'), ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')),
         (_('Inventory Item'), ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')),
         (_('Hardware'), ('manufacturer', 'part_id', 'serial', 'asset_tag')),
         (_('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:
     class Meta:
@@ -1429,22 +1440,17 @@ class InventoryItemForm(DeviceComponentForm):
         component_type = initial.get('component_type')
         component_type = initial.get('component_type')
         component_id = initial.get('component_id')
         component_id = initial.get('component_id')
 
 
-        # Used for picking the default active tab for component selection
-        self.no_component = True
-
         if instance:
         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):
             for component_model in ContentType.objects.filter(MODULAR_COMPONENT_MODELS):
                 if type(instance.component) is component_model.model_class():
                 if type(instance.component) is component_model.model_class():
                     initial[component_model.model] = instance.component
                     initial[component_model.model] = instance.component
-                    self.no_component = False
                     break
                     break
         elif component_type and component_id:
         elif component_type and component_id:
             # When adding the InventoryItem from a component page
             # When adding the InventoryItem from a component page
             if content_type := ContentType.objects.filter(MODULAR_COMPONENT_MODELS).filter(pk=component_type).first():
             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():
                 if component := content_type.model_class().objects.filter(pk=component_id).first():
                     initial[content_type.model] = component
                     initial[content_type.model] = component
-                    self.no_component = False
 
 
         kwargs['initial'] = initial
         kwargs['initial'] = initial
 
 

+ 0 - 2
netbox/dcim/views.py

@@ -2924,14 +2924,12 @@ class InventoryItemView(generic.ObjectView):
 class InventoryItemEditView(generic.ObjectEditView):
 class InventoryItemEditView(generic.ObjectEditView):
     queryset = InventoryItem.objects.all()
     queryset = InventoryItem.objects.all()
     form = forms.InventoryItemForm
     form = forms.InventoryItemForm
-    template_name = 'dcim/inventoryitem_edit.html'
 
 
 
 
 class InventoryItemCreateView(generic.ComponentCreateView):
 class InventoryItemCreateView(generic.ComponentCreateView):
     queryset = InventoryItem.objects.all()
     queryset = InventoryItem.objects.all()
     form = forms.InventoryItemCreateForm
     form = forms.InventoryItemCreateForm
     model_form = forms.InventoryItemForm
     model_form = forms.InventoryItemForm
-    template_name = 'dcim/inventoryitem_edit.html'
 
 
 
 
 @register_model_view(InventoryItem, 'delete')
 @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__ = (
 __all__ = (
+    'FieldGroup',
     'InlineFields',
     '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.field_names = field_names
         self.label = label
         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>
     </div>
   {% endif %}
   {% endif %}
   {% for layout, title, items in rows %}
   {% for layout, title, items in rows %}
+
     {% if layout == 'field' %}
     {% if layout == 'field' %}
       {# Single form field #}
       {# Single form field #}
       {% render_field items.0 %}
       {% render_field items.0 %}
+
     {% elif layout == 'inline' %}
     {% elif layout == 'inline' %}
       {# Multiple form fields on the same line #}
       {# Multiple form fields on the same line #}
       <div class="row mb-3">
       <div class="row mb-3">
@@ -21,6 +23,30 @@
           </div>
           </div>
         {% endfor %}
         {% endfor %}
       </div>
       </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 %}
     {% endif %}
   {% endfor %}
   {% endfor %}
 </div>
 </div>

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

@@ -1,6 +1,6 @@
 from django import template
 from django import template
 
 
-from utilities.forms.rendering import InlineFields
+from utilities.forms.rendering import InlineFields, TabbedFieldGroups
 
 
 __all__ = (
 __all__ = (
     'getfield',
     'getfield',
@@ -58,6 +58,21 @@ def render_fieldset(form, fieldset, heading=None):
             rows.append(
             rows.append(
                 ('inline', item.label, [form[name] for name in item.field_names])
                 ('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:
         else:
             rows.append(
             rows.append(
                 ('field', None, [form[item]])
                 ('field', None, [form[item]])