Enable tabbed group fields in fieldsets

This commit is contained in:
Jeremy Stretch 2024-03-12 17:02:26 -04:00
parent f585c36d86
commit 4c7b6fcec0
6 changed files with 92 additions and 121 deletions

View File

@ -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(_('Outer Dimensions'), 'outer_width', 'outer_depth', 'outer_unit'),
InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')), 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

View File

@ -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')

View File

@ -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 %}

View File

@ -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)
]

View File

@ -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>

View File

@ -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]])