diff --git a/netbox/core/forms/model_forms.py b/netbox/core/forms/model_forms.py index e9cc962cd..464c3eb47 100644 --- a/netbox/core/forms/model_forms.py +++ b/netbox/core/forms/model_forms.py @@ -5,7 +5,7 @@ from django import forms from core.models import * from netbox.forms import NetBoxModelForm from netbox.registry import registry -from utilities.forms import CommentField +from utilities.forms import CommentField, get_field_value __all__ = ( 'DataSourceForm', @@ -44,7 +44,7 @@ class DataSourceForm(NetBoxModelForm): ] if self.backend_fields: fieldsets.append( - ('Backend', self.backend_fields) + ('Backend Parameters', self.backend_fields) ) return fieldsets @@ -52,16 +52,11 @@ class DataSourceForm(NetBoxModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - backend_classes = registry['data_backends'] - - if self.is_bound and self.data.get('type') in backend_classes: - type_ = self.data['type'] - elif self.initial and self.initial.get('type') in backend_classes: - type_ = self.initial['type'] - else: - type_ = self.fields['type'].initial - backend = backend_classes.get(type_) + # Determine the selected backend type + backend_type = get_field_value(self, 'type') + backend = registry['data_backends'].get(backend_type) + # Add backend-specific form fields self.backend_fields = [] for name, form_field in backend.parameters.items(): field_name = f'backend_{name}' diff --git a/netbox/dcim/forms/common.py b/netbox/dcim/forms/common.py index a2243ce2d..f047d621b 100644 --- a/netbox/dcim/forms/common.py +++ b/netbox/dcim/forms/common.py @@ -3,6 +3,7 @@ from django.utils.translation import gettext as _ from dcim.choices import * from dcim.constants import * +from utilities.forms.utils import get_field_value __all__ = ( 'InterfaceCommonForm', @@ -23,6 +24,20 @@ class InterfaceCommonForm(forms.Form): label=_('MTU') ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Determine the selected 802.1Q mode + interface_mode = get_field_value(self, 'mode') + + # Delete VLAN tagging fields which are not relevant for the selected mode + if interface_mode in (InterfaceModeChoices.MODE_ACCESS, InterfaceModeChoices.MODE_TAGGED_ALL): + del self.fields['tagged_vlans'] + elif not interface_mode: + del self.fields['vlan_group'] + del self.fields['untagged_vlan'] + del self.fields['tagged_vlans'] + def clean(self): super().clean() diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 2e7ca0d4b..34f91bbe8 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -1367,6 +1367,13 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm): ] widgets = { 'speed': SelectSpeedWidget(), + 'mode': forms.Select( + attrs={ + 'hx-get': '.', + 'hx-include': '#form_fields input', + 'hx-target': '#form_fields', + } + ), } labels = { 'mode': '802.1Q Mode', diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 2dff8b274..1ba789cf1 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -431,6 +431,12 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView): form = self.initialize_form(request) instance = self.alter_object(self.queryset.model(), request) + # If this is an HTMX request, return only the rendered form HTML + if is_htmx(request): + return render(request, 'htmx/form.html', { + 'form': form, + }) + return render(request, self.template_name, { 'object': instance, 'form': form, diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index e5793c128..d0058eae9 100644 Binary files a/netbox/project-static/dist/netbox.js and b/netbox/project-static/dist/netbox.js differ diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 1ef66b9db..384195df5 100644 Binary files a/netbox/project-static/dist/netbox.js.map and b/netbox/project-static/dist/netbox.js.map differ diff --git a/netbox/project-static/src/forms/index.ts b/netbox/project-static/src/forms/index.ts index 1ef8540fd..f166c75a8 100644 --- a/netbox/project-static/src/forms/index.ts +++ b/netbox/project-static/src/forms/index.ts @@ -1,10 +1,9 @@ import { initFormElements } from './elements'; import { initSpeedSelector } from './speedSelector'; import { initScopeSelector } from './scopeSelector'; -import { initVlanTags } from './vlanTags'; export function initForms(): void { - for (const func of [initFormElements, initSpeedSelector, initScopeSelector, initVlanTags]) { + for (const func of [initFormElements, initSpeedSelector, initScopeSelector]) { func(); } } diff --git a/netbox/project-static/src/forms/vlanTags.ts b/netbox/project-static/src/forms/vlanTags.ts deleted file mode 100644 index 4ad97c363..000000000 --- a/netbox/project-static/src/forms/vlanTags.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { all, getElement, resetSelect, toggleVisibility as _toggleVisibility } from '../util'; - -/** - * Get a select element's containing `.row` element. - * - * @param element Select element. - * @returns Containing row element. - */ -function fieldContainer(element: Nullable): Nullable { - const container = element?.parentElement?.parentElement ?? null; - if (container !== null && container.classList.contains('row')) { - return container; - } - return null; -} - -/** - * Toggle visibility of the select element's container and disable the select element itself. - * - * @param element Select element. - * @param action 'show' or 'hide' - */ -function toggleVisibility>( - element: E, - action: 'show' | 'hide', -): void { - // Find the select element's containing element. - const parent = fieldContainer(element); - if (element !== null && parent !== null) { - // Toggle container visibility to visually remove it from the form. - _toggleVisibility(parent, action); - // Create a new event so that the APISelect instance properly handles the enable/disable - // action. - const event = new Event(`netbox.select.disabled.${element.name}`); - switch (action) { - case 'hide': - // Disable the native select element and dispatch the event APISelect is listening for. - element.disabled = true; - element.dispatchEvent(event); - break; - case 'show': - // Enable the native select element and dispatch the event APISelect is listening for. - element.disabled = false; - element.dispatchEvent(event); - } - } -} - -/** - * Toggle element visibility when the mode field does not have a value. - */ -function handleModeNone(): void { - const elements = [ - getElement('id_tagged_vlans'), - getElement('id_untagged_vlan'), - getElement('id_vlan_group'), - ]; - - if (all(elements)) { - const [taggedVlans, untaggedVlan] = elements; - resetSelect(untaggedVlan); - resetSelect(taggedVlans); - for (const element of elements) { - toggleVisibility(element, 'hide'); - } - } -} - -/** - * Toggle element visibility when the mode field's value is Access. - */ -function handleModeAccess(): void { - const elements = [ - getElement('id_tagged_vlans'), - getElement('id_untagged_vlan'), - getElement('id_vlan_group'), - ]; - if (all(elements)) { - const [taggedVlans, untaggedVlan, vlanGroup] = elements; - resetSelect(taggedVlans); - toggleVisibility(vlanGroup, 'show'); - toggleVisibility(untaggedVlan, 'show'); - toggleVisibility(taggedVlans, 'hide'); - } -} - -/** - * Toggle element visibility when the mode field's value is Tagged. - */ -function handleModeTagged(): void { - const elements = [ - getElement('id_tagged_vlans'), - getElement('id_untagged_vlan'), - getElement('id_vlan_group'), - ]; - if (all(elements)) { - const [taggedVlans, untaggedVlan, vlanGroup] = elements; - toggleVisibility(taggedVlans, 'show'); - toggleVisibility(vlanGroup, 'show'); - toggleVisibility(untaggedVlan, 'show'); - } -} - -/** - * Toggle element visibility when the mode field's value is Tagged (All). - */ -function handleModeTaggedAll(): void { - const elements = [ - getElement('id_tagged_vlans'), - getElement('id_untagged_vlan'), - getElement('id_vlan_group'), - ]; - if (all(elements)) { - const [taggedVlans, untaggedVlan, vlanGroup] = elements; - resetSelect(taggedVlans); - toggleVisibility(vlanGroup, 'show'); - toggleVisibility(untaggedVlan, 'show'); - toggleVisibility(taggedVlans, 'hide'); - } -} - -/** - * Reset field visibility when the mode field's value changes. - */ -function handleModeChange(element: HTMLSelectElement): void { - switch (element.value) { - case 'access': - handleModeAccess(); - break; - case 'tagged': - handleModeTagged(); - break; - case 'tagged-all': - handleModeTaggedAll(); - break; - case '': - handleModeNone(); - break; - } -} - -export function initVlanTags(): void { - const element = getElement('id_mode'); - if (element !== null) { - element.addEventListener('change', () => handleModeChange(element)); - handleModeChange(element); - } -} diff --git a/netbox/templates/dcim/interface_edit.html b/netbox/templates/dcim/interface_edit.html deleted file mode 100644 index a044de660..000000000 --- a/netbox/templates/dcim/interface_edit.html +++ /dev/null @@ -1,101 +0,0 @@ -{% extends 'generic/object_edit.html' %} -{% load form_helpers %} - -{% block form %} - {# Render hidden fields #} - {% for field in form.hidden_fields %} - {{ field }} - {% endfor %} - -
-
-
Interface
-
- {% if form.instance.device %} -
- -
- -
-
- {% endif %} - {% render_field form.module %} - {% render_field form.name %} - {% render_field form.type %} - {% render_field form.speed %} - {% render_field form.duplex %} - {% render_field form.label %} - {% render_field form.description %} - {% render_field form.tags %} -
- -
-
-
Addressing
-
- {% render_field form.vrf %} - {% render_field form.mac_address %} - {% render_field form.wwn %} -
- -
-
-
Operation
-
- {% render_field form.mtu %} - {% render_field form.tx_power %} - {% render_field form.enabled %} - {% render_field form.mgmt_only %} - {% render_field form.mark_connected %} -
- -
-
-
Related Interfaces
-
- {% render_field form.parent %} - {% render_field form.bridge %} - {% render_field form.lag %} -
- - {% if form.instance.is_wireless %} -
-
-
Wireless
-
- {% render_field form.rf_role %} - {% render_field form.rf_channel %} - {% render_field form.rf_channel_frequency %} - {% render_field form.rf_channel_width %} - {% render_field form.wireless_lan_group %} - {% render_field form.wireless_lans %} -
- {% endif %} - -
-
-
Power over Ethernet (PoE)
-
- {% render_field form.poe_mode %} - {% render_field form.poe_type %} -
- -
-
-
802.1Q Switching
-
- {% render_field form.mode %} - {% render_field form.vlan_group %} - {% render_field form.untagged_vlan %} - {% render_field form.tagged_vlans %} -
- - {% if form.custom_fields %} -
-
-
Custom Fields
-
- {% render_custom_fields form %} -
- {% endif %} -{% endblock %} diff --git a/netbox/templates/htmx/form.html b/netbox/templates/htmx/form.html index e5a2ab6c6..e15df4706 100644 --- a/netbox/templates/htmx/form.html +++ b/netbox/templates/htmx/form.html @@ -17,7 +17,7 @@ {% endif %} {% for name in fields %} {% with field=form|getfield:name %} - {% if not field.field.widget.is_hidden %} + {% if field and not field.field.widget.is_hidden %} {% render_field field %} {% endif %} {% endwith %} diff --git a/netbox/utilities/forms/utils.py b/netbox/utilities/forms/utils.py index 1a2f62b2e..2f08a3cce 100644 --- a/netbox/utilities/forms/utils.py +++ b/netbox/utilities/forms/utils.py @@ -12,6 +12,7 @@ __all__ = ( 'expand_alphanumeric_pattern', 'expand_ipaddress_pattern', 'form_from_model', + 'get_field_value', 'get_selected_values', 'parse_alphanumeric_range', 'parse_numeric_range', @@ -113,6 +114,21 @@ def expand_ipaddress_pattern(string, family): yield ''.join([lead, format(i, 'x' if family == 6 else 'd'), remnant]) +def get_field_value(form, field_name): + """ + Return the current bound or initial value associated with a form field, prior to calling + clean() for the form. + """ + field = form.fields[field_name] + + if form.is_bound: + if data := form.data.get(field_name): + if field.valid_value(data): + return data + + return form.get_initial_for_field(field, field_name) + + def get_selected_values(form, field_name): """ Return the list of selected human-friendly values for a form field diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index 089a3ced9..a3523a7cc 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -11,9 +11,12 @@ register = template.Library() @register.filter() def getfield(form, fieldname): """ - Return the specified field of a Form. + Return the specified bound field of a Form. """ - return form[fieldname] + try: + return form[fieldname] + except KeyError: + return None @register.filter(name='widget_type') diff --git a/netbox/virtualization/forms/model_forms.py b/netbox/virtualization/forms/model_forms.py index 403a04d91..e461eac8a 100644 --- a/netbox/virtualization/forms/model_forms.py +++ b/netbox/virtualization/forms/model_forms.py @@ -349,6 +349,15 @@ class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm): labels = { 'mode': '802.1Q Mode', } + widgets = { + 'mode': forms.Select( + attrs={ + 'hx-get': '.', + 'hx-include': '#form_fields input', + 'hx-target': '#form_fields', + } + ), + } help_texts = { 'mode': INTERFACE_MODE_HELP_TEXT, }