Просмотр исходного кода

Merge pull request #22759 from netbox-community/22757-InlineFields-help-text

Closes #22757: Extend InlineFields to support an arbitrary help text
bctiemann 4 недель назад
Родитель
Сommit
d4670266d6

+ 3 - 1
netbox/utilities/forms/rendering.py

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

+ 21 - 13
netbox/utilities/templates/form_helpers/render_fieldset.html

@@ -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 %}

+ 19 - 6
netbox/utilities/templatetags/form_helpers.py

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

+ 17 - 0
netbox/utilities/tests/test_templatetags.py

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