Merge pull request #22759 from netbox-community/22757-InlineFields-help-text
Closes #22757: Extend InlineFields to support an arbitrary help text
This commit is contained in:
commit
d4670266d6
|
|
@ -47,10 +47,12 @@ class InlineFields:
|
|||
Parameters:
|
||||
fields: An iterable of form field names
|
||||
label: The label text to render for the row (optional)
|
||||
help_text: Explanatory text rendered beneath the entire set of fields (optional)
|
||||
"""
|
||||
def __init__(self, *fields, label=None):
|
||||
def __init__(self, *fields, label=None, help_text=None):
|
||||
self.fields = fields
|
||||
self.label = label
|
||||
self.help_text = help_text
|
||||
|
||||
|
||||
class TabbedGroups:
|
||||
|
|
|
|||
|
|
@ -6,28 +6,28 @@
|
|||
<h2 class="col-9 offset-3">{{ heading }}</h2>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for layout, title, items in rows %}
|
||||
{% for row in rows %}
|
||||
|
||||
{% if layout == 'field' %}
|
||||
{% if row.layout == 'field' %}
|
||||
{# Single form field #}
|
||||
{% render_field items.0 %}
|
||||
{% render_field row.items.0 %}
|
||||
|
||||
{% elif layout == 'attribute' %}
|
||||
{% elif row.layout == 'attribute' %}
|
||||
{# A static attribute of the form's instance #}
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-3 col-form-label text-lg-end required">{{ title }}</label>
|
||||
<label class="col-sm-3 col-form-label text-lg-end required">{{ row.title }}</label>
|
||||
<div class="col">
|
||||
<div class="form-control-plaintext">
|
||||
{{ items.0|linkify }}
|
||||
{{ row.items.0|linkify }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% elif layout == 'inline' %}
|
||||
{% elif row.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{% if items|any_required %} required{% endif %}">{{ title|default:'' }}</label>
|
||||
{% for field in items %}
|
||||
<div class="row{% if not row.help_text %} mb-3{% endif %}"{% if row.title %} role="group" aria-label="{{ row.title }}"{% endif %}>
|
||||
<label class="col col-3 col-form-label text-lg-end{% if row.items|any_required %} required{% endif %}">{{ row.title|default:'' }}</label>
|
||||
{% for field in row.items %}
|
||||
<div class="col mb-1">
|
||||
{% render_field_with_aria field has_helptext=True %}
|
||||
<div class="form-text" id="{{ field.auto_id }}_helptext">{% trans field.label %}</div>
|
||||
|
|
@ -39,13 +39,21 @@
|
|||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if row.help_text %}
|
||||
{# Shared help text rendered beneath the entire set of inline fields #}
|
||||
<div class="row mb-3">
|
||||
<div class="col offset-3">
|
||||
<span class="form-text">{{ row.help_text|safe }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% elif layout == 'tabs' %}
|
||||
{% elif row.layout == 'tabs' %}
|
||||
{# Tabbed groups of fields #}
|
||||
<div class="row">
|
||||
<div class="col offset-3">
|
||||
<ul class="nav nav-pills mb-1" role="tablist">
|
||||
{% for tab in items %}
|
||||
{% for tab in row.items %}
|
||||
<li role="presentation" class="nav-item">
|
||||
<button role="tab" type="button" id="{{ tab.id }}_tab" data-bs-toggle="tab" aria-controls="{{ tab.id }}" aria-selected="{% if tab.active %}true{% else %}false{% endif %}" data-bs-target="#{{ tab.id }}" class="nav-link {% if tab.active %}active{% endif %}">
|
||||
{% trans tab.title %}
|
||||
|
|
@ -56,7 +64,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="tab-content p-0 border-0">
|
||||
{% for tab in items %}
|
||||
{% for tab in row.items %}
|
||||
<div class="tab-pane {% if tab.active %}active{% endif %}" id="{{ tab.id }}" role="tabpanel" aria-labelledby="{{ tab.id }}_tab">
|
||||
{% for field in tab.fields %}
|
||||
{% render_field field %}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import warnings
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
from django import forms, template
|
||||
from django.conf import settings
|
||||
|
|
@ -20,6 +22,17 @@ __all__ = (
|
|||
register = template.Library()
|
||||
|
||||
|
||||
class FieldsetRow(NamedTuple):
|
||||
"""
|
||||
A single row within a rendered fieldset. `layout` determines how the row's items are
|
||||
rendered by the template (e.g. 'field', 'inline', 'tabs', 'attribute').
|
||||
"""
|
||||
layout: str
|
||||
items: Sequence
|
||||
title: Any = None
|
||||
help_text: Any = None
|
||||
|
||||
|
||||
#
|
||||
# Filters
|
||||
#
|
||||
|
|
@ -131,7 +144,7 @@ def render_fieldset(form, fieldset):
|
|||
form[name] for name in item.fields if name in form.fields
|
||||
]
|
||||
rows.append(
|
||||
('inline', item.label, fields)
|
||||
FieldsetRow('inline', fields, title=item.label, help_text=item.help_text)
|
||||
)
|
||||
|
||||
# Tabbed groups of fields
|
||||
|
|
@ -148,28 +161,28 @@ def render_fieldset(form, fieldset):
|
|||
if not any(tab['active'] for tab in tabs):
|
||||
tabs[0]['active'] = True
|
||||
rows.append(
|
||||
('tabs', None, tabs)
|
||||
FieldsetRow('tabs', tabs)
|
||||
)
|
||||
|
||||
elif type(item) is M2MAddRemoveFields:
|
||||
if item.name in form.fields:
|
||||
# Simple mode: render a single multi-select field
|
||||
rows.append(
|
||||
('field', None, [form[item.name]])
|
||||
FieldsetRow('field', [form[item.name]])
|
||||
)
|
||||
else:
|
||||
# Add/remove mode: render separate add and remove fields
|
||||
for field_name in (f'add_{item.name}', f'remove_{item.name}'):
|
||||
if field_name in form.fields:
|
||||
rows.append(
|
||||
('field', None, [form[field_name]])
|
||||
FieldsetRow('field', [form[field_name]])
|
||||
)
|
||||
|
||||
elif type(item) is ObjectAttribute:
|
||||
value = getattr(form.instance, item.name)
|
||||
label = value._meta.verbose_name if hasattr(value, '_meta') else item.name
|
||||
rows.append(
|
||||
('attribute', label.title(), [value])
|
||||
FieldsetRow('attribute', [value], title=label.title())
|
||||
)
|
||||
|
||||
# A single form field
|
||||
|
|
@ -179,7 +192,7 @@ def render_fieldset(form, fieldset):
|
|||
if field.name in getattr(form, 'nullable_fields', []):
|
||||
field._nullable = True
|
||||
rows.append(
|
||||
('field', None, [field])
|
||||
FieldsetRow('field', [field])
|
||||
)
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -372,3 +372,20 @@ class RenderFieldsetInlineRequiredTestCase(TestCase):
|
|||
)
|
||||
html = self._render(fieldset)
|
||||
self.assertNotIn('col-form-label text-lg-end required', html)
|
||||
|
||||
def test_inline_help_text_rendered(self):
|
||||
fieldset = FieldSet(
|
||||
InlineFields('optional_field', 'another_optional', label='Combined', help_text='Shared guidance'),
|
||||
)
|
||||
html = self._render(fieldset)
|
||||
# The shared help text is rendered in its own row (col offset-3) beneath the fields
|
||||
self.assertIn('Shared guidance', html)
|
||||
self.assertIn('col offset-3', html)
|
||||
|
||||
def test_inline_help_text_omitted_when_not_provided(self):
|
||||
fieldset = FieldSet(
|
||||
InlineFields('optional_field', 'another_optional', label='Combined'),
|
||||
)
|
||||
html = self._render(fieldset)
|
||||
# With no help text, the shared help-text row (col offset-3) must not be rendered
|
||||
self.assertNotIn('col offset-3', html)
|
||||
|
|
|
|||
Loading…
Reference in New Issue