diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 44c3bb40a..06f28b4e6 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/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 diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index b447ae579..49bbe9be1 100644 --- a/netbox/dcim/views.py +++ b/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') diff --git a/netbox/templates/dcim/inventoryitem_edit.html b/netbox/templates/dcim/inventoryitem_edit.html deleted file mode 100644 index 1dc46ddce..000000000 --- a/netbox/templates/dcim/inventoryitem_edit.html +++ /dev/null @@ -1,107 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load static %} -{% load form_helpers %} -{% load helpers %} -{% load i18n %} - -{% block form %} -
-
-
{% trans "Inventory Item" %}
-
- {% 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 %} -
- -
-
-
{% trans "Hardware" %}
-
- {% render_field form.manufacturer %} - {% render_field form.part_id %} - {% render_field form.serial %} - {% render_field form.asset_tag %} -
- -
-
-
{% trans "Component Assignment" %}
-
-
- -
-
-
- {% render_field form.consoleport %} -
-
- {% render_field form.consoleserverport %} -
-
- {% render_field form.frontport %} -
-
- {% render_field form.interface %} -
-
- {% render_field form.poweroutlet %} -
-
- {% render_field form.powerport %} -
-
- {% render_field form.rearport %} -
-
-
- - {% if form.custom_fields %} -
-
-
{% trans "Custom Fields" %}
-
- {% render_custom_fields form %} -
- {% endif %} -{% endblock %} diff --git a/netbox/utilities/forms/rendering.py b/netbox/utilities/forms/rendering.py index 498b1a2ce..ad87930a9 100644 --- a/netbox/utilities/forms/rendering.py +++ b/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) + ] diff --git a/netbox/utilities/templates/form_helpers/render_fieldset.html b/netbox/utilities/templates/form_helpers/render_fieldset.html index 718a8f6a0..ee1f50293 100644 --- a/netbox/utilities/templates/form_helpers/render_fieldset.html +++ b/netbox/utilities/templates/form_helpers/render_fieldset.html @@ -7,9 +7,11 @@ {% 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 #}
@@ -21,6 +23,30 @@
{% endfor %} + + {% elif layout == 'tabs' %} + {# Tabbed groups of fields #} +
+ +
+
+ {% for tab in items %} +
+ {% for field in tab.fields %} + {% render_field field %} + {% endfor %} +
+ {% endfor %} +
+ {% endif %} {% endfor %} diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index 3f60627b4..47bbaafe8 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/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]])