From c36c690a909914dd1589c5d3652c1ce0a0f842b4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 20 May 2026 16:07:24 -0400 Subject: [PATCH] Fixes #22219: Label inline fields as required if any individual field is required --- .../form_helpers/render_fieldset.html | 2 +- netbox/utilities/templatetags/form_helpers.py | 9 ++++ netbox/utilities/tests/test_templatetags.py | 54 ++++++++++++++++++- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/netbox/utilities/templates/form_helpers/render_fieldset.html b/netbox/utilities/templates/form_helpers/render_fieldset.html index 8941ae2d1..82a17540b 100644 --- a/netbox/utilities/templates/form_helpers/render_fieldset.html +++ b/netbox/utilities/templates/form_helpers/render_fieldset.html @@ -26,7 +26,7 @@ {% elif layout == 'inline' %} {# Multiple form fields on the same line #}
- + {% for field in items %}
{% render_field_with_aria field has_helptext=True %} diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py index 84c1a2a2e..5b5ff0fe8 100644 --- a/netbox/utilities/templatetags/form_helpers.py +++ b/netbox/utilities/templatetags/form_helpers.py @@ -6,6 +6,7 @@ from django.conf import settings from utilities.forms.rendering import InlineFields, M2MAddRemoveFields, ObjectAttribute, TabbedGroups __all__ = ( + 'any_required', 'getfield', 'render_custom_fields', 'render_errors', @@ -34,6 +35,14 @@ def getfield(form, fieldname): return None +@register.filter() +def any_required(fields): + """ + Return True if any of the given bound form fields is required. + """ + return any(getattr(f, 'field', None) and f.field.required for f in fields) + + @register.filter(name='widget_type') def widget_type(field): """ diff --git a/netbox/utilities/tests/test_templatetags.py b/netbox/utilities/tests/test_templatetags.py index fd253744e..b19626f6c 100644 --- a/netbox/utilities/tests/test_templatetags.py +++ b/netbox/utilities/tests/test_templatetags.py @@ -9,8 +9,9 @@ from core.models import ObjectType from dcim.models import Site from extras.choices import CustomFieldTypeChoices from extras.models import CustomField, CustomFieldChoiceSet +from utilities.forms.rendering import FieldSet, InlineFields from utilities.templatetags.builtins.tags import badge, customfield_value, static_with_params -from utilities.templatetags.form_helpers import render_field_with_aria +from utilities.templatetags.form_helpers import any_required, render_field_with_aria, render_fieldset from utilities.templatetags.helpers import _humanize_capacity, humanize_speed @@ -284,3 +285,54 @@ class RenderFieldWithAriaTestCase(TestCase): # No aria-label should be synthesized — an untranslated English fallback # would degrade accessibility under non-English locales. self.assertNotIn('aria-label', html) + + +class AnyRequiredTestCase(TestCase): + """ + Test the any_required template filter. + """ + + class TestForm(forms.Form): + required_field = forms.CharField(required=True) + optional_field = forms.CharField(required=False) + + def test_returns_true_when_any_field_required(self): + form = self.TestForm() + self.assertTrue(any_required([form['optional_field'], form['required_field']])) + + def test_returns_false_when_no_field_required(self): + form = self.TestForm() + self.assertFalse(any_required([form['optional_field']])) + + def test_returns_false_for_empty_list(self): + self.assertFalse(any_required([])) + + +class RenderFieldsetInlineRequiredTestCase(TestCase): + """ + Verify the shared label for an InlineFields row receives the `required` + CSS class when at least one inline field is required. + """ + + class TestForm(forms.Form): + required_field = forms.CharField(required=True) + optional_field = forms.CharField(required=False) + another_optional = forms.CharField(required=False) + + def _render(self, fieldset): + context = render_fieldset(self.TestForm(), fieldset) + return render_to_string('form_helpers/render_fieldset.html', context) + + def test_inline_label_marked_required_when_any_field_required(self): + fieldset = FieldSet( + InlineFields('optional_field', 'required_field', label='Combined'), + ) + html = self._render(fieldset) + self.assertIn('col-form-label text-lg-end required', html) + + def test_inline_label_not_marked_required_when_no_field_required(self): + fieldset = FieldSet( + InlineFields('optional_field', 'another_optional', label='Combined'), + ) + html = self._render(fieldset) + self.assertNotIn('col-form-label text-lg-end required', html)