mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -06:00
Fixes #8058: Display server-side form errors inline with fields
This commit is contained in:
parent
c3dcd8937f
commit
f56e3eb784
@ -1,27 +1,6 @@
|
|||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
|
||||||
<div id="django-messages" class="toast-container">
|
<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 #}
|
{# Non-Field Form Errors #}
|
||||||
{% if form and form.non_field_errors %}
|
{% if form and form.non_field_errors %}
|
||||||
|
@ -48,10 +48,16 @@ class BootstrapMixin:
|
|||||||
]
|
]
|
||||||
|
|
||||||
for field_name, field in self.fields.items():
|
for field_name, field in self.fields.items():
|
||||||
|
css = field.widget.attrs.get('class', '')
|
||||||
|
|
||||||
if field.widget.__class__ not in exempt_widgets:
|
if field.widget.__class__ not in exempt_widgets:
|
||||||
css = field.widget.attrs.get('class', '')
|
field.widget.attrs['class'] = f'{css} form-control'
|
||||||
field.widget.attrs['class'] = ' '.join([css, 'form-control']).strip()
|
|
||||||
|
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):
|
if field.required and not isinstance(field.widget, forms.FileInput):
|
||||||
field.widget.attrs['required'] = 'required'
|
field.widget.attrs['required'] = 'required'
|
||||||
@ -59,13 +65,18 @@ class BootstrapMixin:
|
|||||||
if 'placeholder' not in field.widget.attrs and field.label is not None:
|
if 'placeholder' not in field.widget.attrs and field.label is not None:
|
||||||
field.widget.attrs['placeholder'] = field.label
|
field.widget.attrs['placeholder'] = field.label
|
||||||
|
|
||||||
if field.widget.__class__ == forms.CheckboxInput:
|
def is_valid(self):
|
||||||
css = field.widget.attrs.get('class', '')
|
is_valid = super().is_valid()
|
||||||
field.widget.attrs['class'] = ' '.join((css, 'form-check-input')).strip()
|
|
||||||
|
|
||||||
if field.widget.__class__ == forms.Select:
|
# Apply is-invalid CSS class to fields with errors
|
||||||
css = field.widget.attrs.get('class', '')
|
if not is_valid:
|
||||||
field.widget.attrs['class'] = ' '.join((css, 'form-select')).strip()
|
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'
|
||||||
|
|
||||||
|
return is_valid
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -1,127 +1,65 @@
|
|||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
|
||||||
{% if field|widget_type == 'checkboxinput' %}
|
<div class="row mb-3{% if field.errors %} has-errors{% endif %}">
|
||||||
<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>
|
|
||||||
|
|
||||||
{% elif field|widget_type == 'textarea' and not label %}
|
{# Render the field label, except for: #}
|
||||||
<div class="row mb-3">
|
{# 1. Checkboxes (label appears to the right of the field #}
|
||||||
{% if label %}
|
{# 2. Textareas with no label set (will expand across entire row) #}
|
||||||
<label class="col-sm-3 col-form-label text-lg-end{% if field.field.required %} required{% endif %}" for="{{ field.id_for_label }}">
|
{% if field|widget_type == 'checkboxinput' or field|widget_type == 'textarea' and not label %}
|
||||||
{{ 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 %}
|
||||||
|
|
||||||
|
{# 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>
|
</label>
|
||||||
{% else %}
|
</div>
|
||||||
{% endif %}
|
{# Default field rendering #}
|
||||||
<div class="col">
|
{% else %}
|
||||||
{{ field }}
|
{{ field }}
|
||||||
{% if field.help_text %}
|
{% endif %}
|
||||||
<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>
|
|
||||||
|
|
||||||
{% elif field|widget_type == 'slugwidget' %}
|
{# Display any error messages #}
|
||||||
<div class="row mb-3">
|
{% if field.errors %}
|
||||||
<label class="col-sm-3 col-form-label text-lg-end{% if field.field.required %} required{% endif %}" for="{{ field.id_for_label }}">
|
<div class="form-text text-danger">
|
||||||
{{ label }}
|
{% for error in field.errors %}{{ error }}{% if not forloop.last %}<br />{% endif %}{% endfor %}
|
||||||
</label>
|
</div>
|
||||||
<div class="col">
|
{% elif field.field.required %}
|
||||||
<div class="input-group">
|
<div class="invalid-feedback">
|
||||||
{{ field }}
|
This field is required.
|
||||||
<button id="reslug" type="button" title="Regenerate Slug" class="btn btn-outline-dark border-input">
|
</div>
|
||||||
<i class="mdi mdi-reload"></i>
|
{% endif %}
|
||||||
</button>
|
|
||||||
</div>
|
{# Help text #}
|
||||||
</div>
|
{% if field.help_text %}
|
||||||
</div>
|
<span class="form-text">{{ field.help_text|safe }}</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# 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 %}
|
||||||
|
|
||||||
{% 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>
|
</div>
|
||||||
|
|
||||||
{% elif field|widget_type == 'clearablefileinput' %}
|
</div>
|
||||||
<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>
|
|
||||||
|
|
||||||
{% 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>
|
|
||||||
|
|
||||||
{% 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 %}
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user