فهرست منبع

Fixes #8058: Display server-side form errors inline with fields

jeremystretch 3 سال پیش
والد
کامیت
f56e3eb784
3فایلهای تغییر یافته به همراه73 افزوده شده و 145 حذف شده
  1. 0 21
      netbox/templates/inc/messages.html
  2. 19 8
      netbox/utilities/forms/forms.py
  3. 54 116
      netbox/utilities/templates/form_helpers/render_field.html

+ 0 - 21
netbox/templates/inc/messages.html

@@ -1,27 +1,6 @@
 {% load helpers %}
 
 <div id="django-messages" class="toast-container">
-  {# Django Messages #}
-
-  {# Form Field Errors #}
-  {% if form and form.errors %}
-    {% for field in form %}
-      {% for error in field.errors %}
-        <div class="django-message toast align-items-center border-0 bg-danger" data-django-type="field-error" role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="60000">
-          <div class="toast-header bg-danger">
-            <strong class="me-auto">
-              <i class="mdi mdi-{{ "danger"|icon_from_status }} me-1"></i>
-              {{ field.label }}
-            </strong>
-            <button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
-          </div>
-          <div class="toast-body">
-            {{ error|escape }}
-          </div>
-        </div>
-      {% endfor %}
-    {% endfor %}
-  {% endif %}
 
   {# Non-Field Form Errors #}
   {% if form and form.non_field_errors %}

+ 19 - 8
netbox/utilities/forms/forms.py

@@ -48,10 +48,16 @@ class BootstrapMixin:
         ]
 
         for field_name, field in self.fields.items():
+            css = field.widget.attrs.get('class', '')
 
             if field.widget.__class__ not in exempt_widgets:
-                css = field.widget.attrs.get('class', '')
-                field.widget.attrs['class'] = ' '.join([css, 'form-control']).strip()
+                field.widget.attrs['class'] = f'{css} form-control'
+
+            elif isinstance(field.widget, forms.CheckboxInput):
+                field.widget.attrs['class'] = f'{css} form-check-input'
+
+            elif isinstance(field.widget, forms.Select):
+                field.widget.attrs['class'] = f'{css} form-select'
 
             if field.required and not isinstance(field.widget, forms.FileInput):
                 field.widget.attrs['required'] = 'required'
@@ -59,13 +65,18 @@ class BootstrapMixin:
             if 'placeholder' not in field.widget.attrs and field.label is not None:
                 field.widget.attrs['placeholder'] = field.label
 
-            if field.widget.__class__ == forms.CheckboxInput:
-                css = field.widget.attrs.get('class', '')
-                field.widget.attrs['class'] = ' '.join((css, 'form-check-input')).strip()
+    def is_valid(self):
+        is_valid = super().is_valid()
+
+        # Apply is-invalid CSS class to fields with errors
+        if not is_valid:
+            for field_name in self.errors:
+                # Ignore e.g. __all__
+                if field := self.fields.get(field_name):
+                    css = field.widget.attrs.get('class', '')
+                    field.widget.attrs['class'] = f'{css} is-invalid'
 
-            if field.widget.__class__ == forms.Select:
-                css = field.widget.attrs.get('class', '')
-                field.widget.attrs['class'] = ' '.join((css, 'form-select')).strip()
+        return is_valid
 
 
 #

+ 54 - 116
netbox/utilities/templates/form_helpers/render_field.html

@@ -1,127 +1,65 @@
 {% load form_helpers %}
 {% load helpers %}
 
-{% if field|widget_type == 'checkboxinput' %}
-    <div class="row mb-3">
-        <div class="col-sm-3"></div>
-        <div class="col">
-            <div class="form-check{% if field.errors %} has-error{% endif %}">
-                {{ field }}
-                <label for="{{ field.id_for_label }}" class="form-check-label">
-                    {{ label }}
-                </label>
-            </div>
-            {% if field.help_text %}
-                <span class="form-text">{{ field.help_text|safe }}</span>
-            {% endif %}
-            {% if bulk_nullable %}
-                <div class="form-check my-1">
-                    <input type="checkbox" class="form-check-input" name="_nullify" value="{{ field.name }}" />
-                    <label class="form-check-label">Set Null</label>
-                </div>
-            {% endif %}
-        </div>
-    </div>
+<div class="row mb-3{% if field.errors %} has-errors{% endif %}">
 
-{% elif field|widget_type == 'textarea' and not label %}
-    <div class="row mb-3">
-        {% if label %}
-        <label class="col-sm-3 col-form-label text-lg-end{% if field.field.required %} required{% endif %}" for="{{ field.id_for_label }}">
-            {{ label }}
-        </label>
-        {% else %}
-        {% endif %}
-        <div class="col">
-            {{ field }}
-            {% if field.help_text %}
-                <span class="form-text">{{ field.help_text|safe }}</span>
-            {% endif %}
-            {% if bulk_nullable %}
-                <div class="form-check my-1">
-                    <input type="checkbox" class="form-check-input" name="_nullify" value="{{ field.name }}" />
-                    <label class="form-check-label">Set Null</label>
-                </div>
-            {% endif %}
-        </div>
-    </div>
+  {# Render the field label, except for: #}
+  {#   1. Checkboxes (label appears to the right of the field #}
+  {#   2. Textareas with no label set (will expand across entire row) #}
+  {% if field|widget_type == 'checkboxinput' or field|widget_type == 'textarea' and not label %}
+  {% else %}
+    <label for="{{ field.id_for_label }}" class="col-sm-3 col-form-label text-lg-end{% if field.field.required %} required{% endif %}">
+      {{ label }}
+    </label>
+  {% endif %}
 
-{% elif field|widget_type == 'slugwidget' %}
-    <div class="row mb-3">
-        <label class="col-sm-3 col-form-label text-lg-end{% if field.field.required %} required{% endif %}" for="{{ field.id_for_label }}">
-            {{ label }}
+  {# Render the field itself #}
+  <div class="col{% if field|widget_type == 'checkboxinput' %} offset-3{% endif %}">
+    {# Include the "regenerate" button on slug fields #}
+    {% if field|widget_type == 'slugwidget' %}
+      <div class="input-group">
+        {{ field }}
+        <button id="reslug" type="button" title="Regenerate Slug" class="btn btn-outline-dark border-input">
+          <i class="mdi mdi-reload"></i>
+        </button>
+      </div>
+    {# Render checkbox labels to the right of the field #}
+    {% elif field|widget_type == 'checkboxinput' %}
+      <div class="form-check">
+        {{ field }}
+        <label for="{{ field.id_for_label }}" class="form-check-label">
+          {{ label }}
         </label>
-        <div class="col">
-            <div class="input-group">
-                {{ field }}
-                <button id="reslug" type="button" title="Regenerate Slug" class="btn btn-outline-dark border-input">
-                    <i class="mdi mdi-reload"></i>
-                </button>
-            </div>
-        </div>
-    </div>
+      </div>
+    {# Default field rendering #}
+    {% else %}
+      {{ field }}
+    {% endif %}
 
-{% elif field|widget_type == 'fileinput' %}
-  <div class="input-group mb-3">
-    <input
-        class="form-control"
-        type="file"
-        name="{{ field.name }}"
-        placeholder="{{ field.placeholder }}"
-        id="id_{{ field.name }}"
-        accept="{{ field.field.widget.attrs.accept }}"
-        {% if field.is_required %}required{% endif %}
-    />
-    <label for="{{ field.id_for_label }}" class="input-group-text">{{ label|bettertitle }}</label>
-  </div>
+    {# Display any error messages #}
+    {% if field.errors %}
+      <div class="form-text text-danger">
+        {% for error in field.errors %}{{ error }}{% if not forloop.last %}<br />{% endif %}{% endfor %}
+      </div>
+    {% elif field.field.required %}
+      <div class="invalid-feedback">
+        This field is required.
+      </div>
+    {% endif %}
 
-{% elif field|widget_type == 'clearablefileinput' %}
-    <div class="row mb-3">
-        <label for="{{ field.id_for_label }}" class="form-label col col-md-3 text-lg-end{% if field.field.required %} required{% endif %}">
-            {{ label }}
-        </label>
-        <div class="col col-md-9">
-            {{ field }}
-        </div>
-    </div>
+    {# Help text #}
+    {% if field.help_text %}
+      <span class="form-text">{{ field.help_text|safe }}</span>
+    {% endif %}
 
-{% elif field|widget_type == 'selectmultiple' %}
-    <div class="row mb-3">
-        <label for="{{ field.id_for_label }}" class="form-label col col-md-3 text-lg-end{% if field.field.required %} required{% endif %}">
-            {{ label }}
-        </label>
-        <div class="col col-md-9">
-            {{ field }}
-            {% if bulk_nullable %}
-                <div class="form-check my-1">
-                    <input type="checkbox" class="form-check-input" name="_nullify" value="{{ field.name }}" />
-                    <label class="form-check-label">Set Null</label>
-                </div>
-            {% endif %}
-        </div>
-    </div>
+    {# For bulk edit forms, include an option to nullify the field #}
+    {% if bulk_nullable %}
+      <div class="form-check my-1">
+        <input type="checkbox" class="form-check-input" name="_nullify" value="{{ field.name }}" id="nullify_{{ field.id_for_label }}" />
+        <label for="nullify_{{ field.id_for_label }}" class="form-check-label">Set Null</label>
+      </div>
+    {% endif %}
 
-{% else %}
-    <div class="row mb-3">
-        <label for="{{ field.id_for_label }}" class="col-sm-3 col-form-label text-lg-end{% if field.field.required %} required{% endif %}">
-            {{ label }}
-        </label>
-        <div class="col">
-            {{ field }}
-            {% if field.help_text %}
-                <span class="form-text">{{ field.help_text|safe }}</span>
-            {% endif %}
-            <div class="invalid-feedback">
-                {% if field.field.required %}
-                    <strong>{{ label }}</strong> field is required.
-                {% endif %}
-            </div>
-            {% if bulk_nullable %}
-                <div class="form-check my-1">
-                    <input type="checkbox" class="form-check-input" name="_nullify" value="{{ field.name }}" />
-                    <label class="form-check-label">Set Null</label>
-                </div>
-            {% endif %}
-        </div>
-    </div>
-{% endif %}
+  </div>
 
+</div>