diff --git a/netbox/circuits/forms/model_forms.py b/netbox/circuits/forms/model_forms.py index 9e29f6477..d73da3a02 100644 --- a/netbox/circuits/forms/model_forms.py +++ b/netbox/circuits/forms/model_forms.py @@ -7,7 +7,7 @@ from ipam.models import ASN from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField -from utilities.forms.rendering import TabbedFieldGroups +from utilities.forms.rendering import TabbedGroups from utilities.forms.widgets import DatePicker, NumberWithOptions __all__ = ( @@ -153,7 +153,7 @@ class CircuitTerminationForm(NetBoxModelForm): 'term_side', 'description', 'tags', - TabbedFieldGroups( + TabbedGroups( (_('Site'), 'site'), (_('Provider Network'), 'provider_network'), ), diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 06f28b4e6..e0c25dbba 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, TabbedFieldGroups +from utilities.forms.rendering import InlineFields, TabbedGroups 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 Dimensions'), 'outer_width', 'outer_depth', 'outer_unit'), - InlineFields(_('Weight'), 'weight', 'max_weight', 'weight_unit'), + InlineFields('outer_width', 'outer_depth', 'outer_unit', label=_('Outer Dimensions')), + InlineFields('weight', 'max_weight', 'weight_unit', label=_('Weight')), 'mounting_depth', 'desc_units', )), @@ -1415,7 +1415,7 @@ class InventoryItemForm(DeviceComponentForm): (_('Inventory Item'), ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')), (_('Hardware'), ('manufacturer', 'part_id', 'serial', 'asset_tag')), (_('Component Assignment'), ( - TabbedFieldGroups( + TabbedGroups( (_('Interface'), 'interface'), (_('Console Port'), 'consoleport'), (_('Console Server Port'), 'consoleserverport'), diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index 85f9591a8..0aba37fb9 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -16,7 +16,7 @@ from utilities.forms.fields import ( CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField, SlugField, ) -from utilities.forms.rendering import InlineFields, ObjectAttribute, TabbedFieldGroups +from utilities.forms.rendering import InlineFields, ObjectAttribute, TabbedGroups from utilities.forms.widgets import DatePicker from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface @@ -312,7 +312,7 @@ class IPAddressForm(TenancyForm, NetBoxModelForm): (_('IP Address'), ('address', 'status', 'role', 'vrf', 'dns_name', 'description', 'tags')), (_('Tenancy'), ('tenant_group', 'tenant')), (_('Assignment'), ( - TabbedFieldGroups( + TabbedGroups( (_('Device'), 'interface'), (_('Virtual Machine'), 'vminterface'), (_('FHRP Group'), 'fhrpgroup'), @@ -725,12 +725,12 @@ class ServiceForm(NetBoxModelForm): fieldsets = ( (_('Service'), ( - TabbedFieldGroups( + TabbedGroups( (_('Device'), 'device'), (_('Virtual Machine'), 'virtual_machine'), ), 'name', - InlineFields(_('Port(s)'), 'protocol', 'ports'), + InlineFields('protocol', 'ports', label=_('Port(s)')), 'ipaddresses', 'description', 'tags', @@ -753,11 +753,11 @@ class ServiceCreateForm(ServiceForm): fieldsets = ( (_('Service'), ( - TabbedFieldGroups( + TabbedGroups( (_('Device'), 'device'), (_('Virtual Machine'), 'virtual_machine'), ), - TabbedFieldGroups( + TabbedGroups( (_('From Template'), 'service_template'), (_('Custom'), 'name', 'protocol', 'ports'), ), diff --git a/netbox/templates/htmx/form.html b/netbox/templates/htmx/form.html index 0bfcb00ca..f9eecc2b9 100644 --- a/netbox/templates/htmx/form.html +++ b/netbox/templates/htmx/form.html @@ -9,8 +9,8 @@ {% endfor %} {# Render grouped fields according to Form #} - {% for group, items in form.fieldsets %} - {% render_fieldset form items heading=group %} + {% for fieldset in form.fieldsets %} + {% render_fieldset form fieldset %} {% endfor %} {% if form.custom_fields %} diff --git a/netbox/utilities/forms/rendering.py b/netbox/utilities/forms/rendering.py index d60f3f061..ea73c38ff 100644 --- a/netbox/utilities/forms/rendering.py +++ b/netbox/utilities/forms/rendering.py @@ -3,28 +3,39 @@ import string from functools import cached_property __all__ = ( + 'FieldSet', 'InlineFields', 'ObjectAttribute', - 'TabbedFieldGroups', + 'TabbedGroups', ) -class FieldGroup: +class FieldSet: + """ + A generic grouping of fields, with an optional name. Each field will be rendered + on its own row under the heading (name). + """ + def __init__(self, *fields, name=None): + self.fields = fields + self.name = name - def __init__(self, label, *field_names): - self.field_names = field_names + +class InlineFields: + """ + A set of fields rendered inline (side-by-side) with a shared label; typically nested within a FieldSet. + """ + def __init__(self, *fields, label=None): + self.fields = fields self.label = label -class InlineFields(FieldGroup): - pass - - -class TabbedFieldGroups: - +class TabbedGroups: + """ + Two or more groups of fields (FieldSets) arranged under tabs among which the user can navigate. + """ def __init__(self, *groups): self.groups = [ - FieldGroup(*group) for group in groups + FieldSet(*group, name=name) for name, *group in groups ] # Initialize a random ID for the group (for tab selection) @@ -37,13 +48,15 @@ class TabbedFieldGroups: return [ { 'id': f'{self.id}_{i}', - 'title': group.label, - 'fields': group.field_names, + 'title': group.name, + 'fields': group.fields, } for i, group in enumerate(self.groups, start=1) ] class ObjectAttribute: - + """ + Renders the value for a specific attribute on the form's instance. + """ def __init__(self, name): self.name = name diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index e336ac21b..48a1a5aa8 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, ObjectAttribute, TabbedFieldGroups +from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, TabbedGroups __all__ = ( 'getfield', @@ -48,24 +48,29 @@ def widget_type(field): # @register.inclusion_tag('form_helpers/render_fieldset.html') -def render_fieldset(form, fieldset, heading=None): +def render_fieldset(form, fieldset): """ Render a group set of fields. """ + # Handle legacy tuple-based fieldset definitions, e.g. (_('Label'), ('field1, 'field2', 'field3')) + if type(fieldset) is not FieldSet: + name, fields = fieldset + fieldset = FieldSet(*fields, name=name) + rows = [] - for item in fieldset: + for item in fieldset.fields: # Multiple fields side-by-side if type(item) is InlineFields: fields = [ - form[name] for name in item.field_names if name in form.fields + form[name] for name in item.fields if name in form.fields ] rows.append( ('inline', item.label, fields) ) # Tabbed groups of fields - elif type(item) is TabbedFieldGroups: + elif type(item) is TabbedGroups: tabs = [ { 'id': tab['id'], @@ -95,7 +100,7 @@ def render_fieldset(form, fieldset, heading=None): ) return { - 'heading': heading, + 'heading': fieldset.name, 'rows': rows, } diff --git a/netbox/vpn/forms/model_forms.py b/netbox/vpn/forms/model_forms.py index efb8a7eda..9674ee2f9 100644 --- a/netbox/vpn/forms/model_forms.py +++ b/netbox/vpn/forms/model_forms.py @@ -7,7 +7,7 @@ from ipam.models import IPAddress, RouteTarget, VLAN from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField -from utilities.forms.rendering import TabbedFieldGroups +from utilities.forms.rendering import TabbedGroups from utilities.forms.utils import add_blank_choice, get_field_value from utilities.forms.widgets import HTMXSelect from virtualization.models import VirtualMachine, VMInterface @@ -448,7 +448,7 @@ class L2VPNTerminationForm(NetBoxModelForm): fieldsets = ( (None, ( 'l2vpn', - TabbedFieldGroups( + TabbedGroups( (_('VLAN'), 'vlan'), (_('Device'), 'interface'), (_('Virtual Machine'), 'vminterface'),