Merge pull request #22257 from netbox-community/22219-required-inline-fields

Fixes #22219: Label inline fields as required if any individual field is required
This commit is contained in:
bctiemann 2026-05-22 09:21:06 -04:00 committed by GitHub
commit 59678d0ca5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 63 additions and 2 deletions

View File

@ -26,7 +26,7 @@
{% elif layout == 'inline' %}
{# Multiple form fields on the same line #}
<div class="row mb-3"{% if title %} role="group" aria-label="{{ title }}"{% endif %}>
<label class="col col-3 col-form-label text-lg-end">{{ title|default:'' }}</label>
<label class="col col-3 col-form-label text-lg-end{% if items|any_required %} required{% endif %}">{{ title|default:'' }}</label>
{% for field in items %}
<div class="col mb-1">
{% render_field_with_aria field has_helptext=True %}

View File

@ -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):
"""

View File

@ -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)