mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-25 01:48:38 -06:00
Enable tabbed group fields in fieldsets
This commit is contained in:
parent
f585c36d86
commit
4c7b6fcec0
@ -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
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
@ -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 %}
|
|
@ -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)
|
||||||
|
]
|
||||||
|
@ -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>
|
||||||
|
@ -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]])
|
||||||
|
Loading…
Reference in New Issue
Block a user