How to use "error_css_class"?

Hello. I’m coding a form in Django for users to input their name, last name and email. I define a form class for this, with the three fields, adding each one of them the class “form-control” form bootstrap to give them some style.
The problem is that I want those fields to be styled differently when the user inputs invalid data into them and submits the form (eg.: a red border and light red color for the field). I’ve tried using the “error_css_class” to add a class to the fields that contain errors, so that I can style them with CSS. But for some reason, when I input wrong data into the fields, the error class is never added to it, so it doesn’t get styled.

Here is the code of the forms.py file, where the form and its fields are declared, along with the clean methods to validate them:

from django import forms
from .models import Turno

class TurnoForm(forms.ModelForm):
        
    class Meta:
        model = Turno
        fields = ['nombre', 'apellido', 'email', 'fecha', 'hora']
        widgets = {
            "nombre": forms.TextInput(attrs={"class": "form-control", "name": "nombre"}),
            "apellido": forms.TextInput(attrs={"class": "form-control", "name": "apellido"}),
            "email": forms.EmailInput(attrs={"class": "form-control", "name": "email"}),
        }
       
        error_css_class = "is-invalid"
                
    def clean_nombre(self):
        nombre = self.cleaned_data["nombre"]

        if not nombre.isalpha():
            raise forms.ValidationError("Por favor, ingrese solo caracteres alfabéticos", code="carac_esp")
            
        
        return nombre
    
    def clean_apellido(self):
        apellido = self.cleaned_data["apellido"]

        if not apellido.isalpha():
            raise forms.ValidationError("Por favor, ingrese solo caracteres alfabéticos", code="carac_esp")
        
                    
        return apellido
    
    def clean_email(self):
        email = self.cleaned_data["email"]
        dominios_permitidos = ['gmail.com', 'hotmail.com', 'yahoo.com']

        if not any(email.endswith(dominio) for dominio in dominios_permitidos):
           raise forms.ValidationError("Por favor, ingrese una dirección de e-mail válida", code="email_invalido")
        
        return email
        

Can anyone tell me what I’m doing wrong? I would really appreciate it, since I’ven stuck with this problem for 3 months now.

Hi, in your code, error_css_class is part of the Meta, but it is supposed to be a class attribute of the form.

Hello Antoine! I tried placing the error_css_class as a class atribute of the form, outside the Meta class, but the result is the same. For some reason, the “is-invalid” class won’t get added to the fields even if they are incorrectly completed.

Please post your view that is rendering and processing this form.

Hi Ken! The code of the views.py file is:

from django.shortcuts import render
from .forms import TurnoForm

def home(request):

    if request.method == "POST":
        form = TurnoForm(request.POST)
        
        if form.is_valid():
            form.save()
            return render(request, "home/index2.html", {"form": form})
               
    
    else:

        form = TurnoForm()
    
    return render(request, "home/index2.html", {"form": form})

Please post the index.html template.

You might also want to add a print call before that last return render... statement to print the form. (Perhaps print(form.as_p()) to verify that the form is as it should be.)

Here is the code of the index template:

{% load static %}
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Sitio web dental xxxx</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
    <link rel="stylesheet" href="{% static 'home/css/style2.css' %}">
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
    <link href="https://fonts.googleapis.com/css2?family=Ubuntu+Condensed&display=swap" rel="stylesheet">
    <!-- Agrega la hoja de estilos de Flatpickr -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
    <!-- Agrega la biblioteca de Flatpickr -->
    <script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
   
</head>
<body>
    <style>
        body{
            background-image: url("static/home/img/foto_bg.png");
            background-repeat: no-repeat;
            background-size: 100% 100%;
            overflow-x: hidden;
        }
    </style>
    <header>

        <div id="div_titles">

            <h1 id="title">Od. xxxx</h1>
            <h2 id="subtitle">Ortodoncia y ortopedia maxilar</h2>
            
        </div>
<!--
<div id="div_sinopsis">

            <pre id="p1">
                Nos especializamos en el campo de la ortodoncia y ortopedia maxilar, brindando atención
                a niños y adultos.
                Además realizamos trabajos de odontología general
        </pre>

        </div>
-->
        

    </header>

    <div id="div_presentation">
        
        <img src="{% static 'home/img/foto_presentation.png' %}" id="img_presentation">

        <div id="div_description">
            <p id="title_presentation">Od. xxxx</p>
        
            <div id="descr_presentation">
                <p>M.N. xxx</p>
                <p>Odontóloga Universidad de Buenos Aires.</p>
                <p>Ayudante de la Cátedra de Cirugía y Traumatología Bucomaxilofacial II,</p>
                <p>Fac. de Odontología, Universidad de Buenos Aires</p>
            </div>

        </div>
        
        <div id="div_ortodoncia">

            <h3>Trabajos de ortodoncia</h3>
            
            <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Sed ullam consequatur nulla in dicta dignissimos
                tenetur asperiores illo. Laudantium qui perferendis molestiae sapiente saepe dolores placeat similique
                accusamus cumque possimus?</p>
            

        </div>

    </div>
    
    <div id="div_otras_especialidades">

        <h2 id="h2_otros_servicios">Otros servicios</h2>

    </div>

    <div id="div_otros_servicios">

        <div id="div_limp_dent" class="servicios">
            <img src="{% static 'home/img/s6.png' %}" id="img_limp_dent">
            <h3 class="h3_servicio limp_dent">Limpieza dental con ultrasonido</h3>
        </div>

        <div id="div_blanq" class="servicios">
            <img src="{% static 'home/img/s6.png' %}" id="img_blanq">
            <h3 class="h3_servicio blanq">Blanqueamiento</h3>
        </div>

        <div id="div_restaur" class="servicios">
            <img src="{% static 'home/img/s6.png' %}" id="img_restaur">
            <h3 class="h3_servicio restaur">Restauración dental</h3>
        </div>

        <div id="div_tto_cond" class="servicios">
            <img src="{% static 'home/img/s6.png' %}" id="img_tto_cond">
            <h3 class="h3_servicio tto_cond">Tratamiento de conducto</h3>
        </div>

        <div id="div_extracc" class="servicios">
            <img src="{% static 'home/img/s6.png' %}" id="img_extracc">
            <h3 class="h3_servicio extracc">Extracciones</h3>
        </div>

        <div id="div_protesis" class="servicios">
            <img src="{% static 'home/img/s6.png' %}" id="img_protesis">
            <h3 class="h3_servicio protesis">Prótesis fija y removible</h3>
        </div>

        <div id="div_brux" class="servicios">
            <img src="{% static 'home/img/s6.png' %}" id="img_brux">
            <h3 class="h3_servicio brux">Placa para bruxismo</h3>
        </div>

        <div id="div_top_fluor" class="servicios">
            <img src="{% static 'home/img/s6.png' %}" id="img_top_fluor">
            <h3 class="h3_servicio top_fluor">Topicación con flúor</h3>
        </div>

    </div>

<div id="turnos">
    <fieldset>
        <legend id="legend-turnos">Agende su turno</legend>
        <div id="div_formbox">
            <form method="post" id="form_turnos">
                <div class="form-group">
                    {% csrf_token %}
                    
                    {{ form.non_field_errors }}
                    
                    <div id="div_calendario">

                    </div>
            
                    <div class="input_group">
                        
                        
                        <div class="input_field">
                            {{form.nombre}}
                        </div>
                        
                        <div class="input_field">
                            {{form.apellido}}
                        </div>
                                                    
                        
                        <div class="input_field">
                            {{form.email}}
                        </div>
                                                          
                        
                        <div class="input_field">
                            <input type="text" class="form-control" name="fecha" id="id_fecha" placeholder="Fecha" readonly>
                        </div>
        
                        <div class="input_field">
                            <input type="text" class="form-control" name="hora" id="id_hora" placeholder="Hora" readonly>
                        </div>

                        <button type="submit" class="btn btn-primary" id="btn_guardar">Guardar</button>
        
                    </div>
        
                </div>
            </form>
        </div>
    
        <div id="div_errores">
            {% if form.nombre.errors %}
                <p id="error_nombre">{{ form.nombre.errors }}</p>
            {% endif %}
            
            {% if form.apellido.errors %}
                <p id="error_apellido">{{ form.apellido.errors }}</p>
            {% endif %}
            
            {% if form.email.errors %}
                <p id="error_email">{{ form.email.errors }}</p>
            {% endif %}
            
            {% if form.fecha.errors %}
                <p id="error_fecha">{{ form.fecha.errors }}</p>
            {% endif %}
            
            {% if form.hora.errors %}
                <p id="error_hora">{{ form.hora.errors }}</p>
            {% endif %}
        </div>
    </fieldset>
</div>
        
    
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            var calendario = document.getElementById("div_calendario");
            var fechaInput = document.getElementById("id_fecha");
            var horaInput = document.getElementById("id_hora");
    
            flatpickr(calendario, {
                enableTime: true,
                dateFormat: "Y-m-d H:i",
                inline: true,
                appendTo: calendario,
                onChange: function(selectedDates, dateStr, instance) {
                    if (selectedDates.length > 0) {
                        fechaInput.value = instance.formatDate(selectedDates[0], "Y-m-d");
                        horaInput.value = instance.formatDate(selectedDates[0], "H:i");
                    }
                }
            });
        });
    </script>

    
  
</body>
<footer>
    
    <div id="div_contacto">
        <p>Contáctenos:</p>
        <div id="div_instagram">
            <a id="link_instagram" href="xxxx"><img id="img_instagram" src="{% static 'home/img/instagram_icon.png' %}"/>od.marinadallavecchia</a>
        </div>
        <div id="div_whatsapp">
            <p><img id="img_whatsapp" src="{% static 'home/img/whatsapp_icon.png' %}">1130346074</p>
        </div>
       
    </div>
   
</footer>
</html>

I did the print of the form and everything appears to be fine. Perhaps you can find some error in my index file.

When manually rendering fields, the use of form.field only renders the widget.

The fields classes (including error class) are usually set on the element containing the widget (e.g. the <div> element if you use form.as_div).

Here, you want to set the classes on your surrounding <div class="input_field"> element. To get the classes to apply to field, you can use form.field.css_classes (see The Forms API | Django documentation | Django).

You can use something like

<div class="input_field {{ form.field.css_classes}}">

Thanks for your answer! I think it’s a step in the right direction. Doing it like you suggested, now the error class (is-invalid) is being applied to the div element containing the widget. However, the div doesn’t show any of the styling of that error class. I know the class was applied to the div because I can see it when I check out the browser’s console, but the div remains white and with no borders like it should have because of the error class.

Sorry, I didn’t pay attention to the fact you are using bootstrap, and so the is-invalid class must be set on the widget itself.

As a consequence, you may set the is-invalid class directly in widget attrs for invalid fields. You may override the form’s full_clean method for that:

    def full_clean(self):
        super().full_clean()
        for field_name, errors in self.errors.items():
            if field_name in self.fields:
                classes = self.fields[field_name].widget.attrs.get("class")
                if classes:
                    classes += f" {self.error_css_class}"
                else:
                    classes = self.error_css_class
                self.fields[field_name].widget.attrs["class"] = classes

This can appear a little tricky but django is not specifically designed for integration with bootstrap. If you want advanced integration of your forms with bootstrap, you may consider using third party libraries like django-crispy-forms.

Hope that helps.

Hello Antoine! I took your suggestion and added the functionality of crispy forms (I’m really new to this, so I didn’t even know of it). The styling of the error looks great now: the colors of the fields change the way I wanted when you input wrong data. But now I have another problem (and a really weird one, by the way): when you input invalid data into any of the form fields and try to submit it, ALL fields get styled with the is-invalid error class.
I don’t know why this happens, since in my forms.py file I use the clean methods to raise a ValidationError in the field only when a certain condition is met. It’s strange that all of them would raise an error when only one of them is incorrect.

Can you share the code for your form ?

Also can you check what are the errors in your form after validation (you can add a print(form.errors) in your view), and can you show the rendered Html of the form, in which there are unexpectef is-invalid classes set ?

The code of my form in the HTML file is:

 <form method="post" id="form_turnos">
                <div class="form-group">
                    {% csrf_token %}
                    
                    {{ form.non_field_errors }}
                                
                    <div class="input_group">
                        
                        {% crispy form %}
                          
                        <button type="submit" class="btn btn-primary" id="btn_guardar">Guardar</button>
        
                    </div>
        
                </div>
            </form>

When I use print(form.errors) and I input invalid data in either the nombre or the apellido fields, the console gives me this:

<ul class="errorlist"><li>nombre<ul class="errorlist"><li>Por favor, ingrese solo caracteres alfabéticos</li></ul></li><li>apellido<ul class="errorlist"><li>Por favor, ingrese solo caracteres alfabéticos</li></ul></li></ul>

And here is the rendered HTML form after an invalid input in the nombre field:

<form  method="post" > <input type="hidden" name="csrfmiddlewaretoken" value="T3n5OfuCSLicESjyosuMCiGBN5Hoh59XvaYvTncXT8CHP7feV2nUcxCzFBYk4gdT"> <div id="div_id_nombre" class="form-group is-invalid"> <label for="id_nombre" class=" requiredField">
                Nombre<span class="asteriskField">*</span> </label> <div> <input type="text" name="nombre" value="Juan45" class="form-control textinput is-invalid" name="nombre" maxlength="50" required aria-invalid="true" id="id_nombre"> <span id="error_1_id_nombre" class="invalid-feedback"><strong>Por favor, ingrese solo caracteres alfabéticos</strong></span> </div> </div> <div id="div_id_apellido" class="form-group is-invalid"> <label for="id_apellido" class=" requiredField">
                Apellido<span class="asteriskField">*</span> </label> <div> <input type="text" name="apellido" value="Perez" class="form-control textinput is-invalid" name="apellido" maxlength="50" required aria-invalid="true" id="id_apellido"> <span id="error_1_id_apellido" class="invalid-feedback"><strong>Por favor, ingrese solo caracteres alfabéticos</strong></span> </div> </div> <div id="div_id_email" class="form-group"> <label for="id_email" class=" requiredField">
                Email<span class="asteriskField">*</span> </label> <div> <input type="email" name="email" value="someemail@gmail.com" class="form-control emailinput" name="email" maxlength="30" required id="id_email"> </div> </div> <div id="div_id_fecha" class="form-group"> <label for="id_fecha" class=" requiredField">
                Fecha<span class="asteriskField">*</span> </label> <div> <input type="text" name="fecha" value="2024-06-26" readonly class="dateinput form-control" required id="id_fecha"> </div> </div> <div id="div_id_hora" class="form-group"> <label for="id_hora" class=" requiredField">
                Hora<span class="asteriskField">*</span> </label> <div> <input type="text" name="hora" value="12:00" readonly class="timeinput form-control" required id="id_hora"> </div> </div> </form>

As you can see, I added the number 45 to the nombre field and then submitted the form, but for some reason also the apellido field gets the class is-invalid.

So, the rendered html is consistent with the content of form.errors, hence the problem is in the errors raised by the form. Can you share the complete python code of your Form, please ? Maybe also code of your Turno model if there are some fields validations method in it too.

In particular, check that when cleaning the apellido field, you don’t retrieve value from nombre field in cleaned data.

The code of my forms.py file is:

from django import forms
from .models import Turno

class TurnoForm(forms.ModelForm):

    error_css_class = "is-invalid"
        
    class Meta:
        model = Turno
        fields = ['nombre', 'apellido', 'email', 'fecha', 'hora']
        widgets = {
            "nombre": forms.TextInput(attrs={"class": "form-control", "name": "nombre"}),
            "apellido": forms.TextInput(attrs={"class": "form-control", "name": "apellido"}),
            "email": forms.EmailInput(attrs={"class": "form-control", "name": "email"}),
        }
       
    def __init__(self, *args, **kwargs):
        super(TurnoForm, self).__init__(*args, **kwargs)
        self.fields['fecha'].widget.attrs['readonly'] = True
        self.fields['hora'].widget.attrs['readonly'] = True
                
    def clean_nombre(self):
        nombre = self.cleaned_data["nombre"]

        if not nombre.isalpha():
            raise forms.ValidationError("Por favor, ingrese solo caracteres alfabéticos", code="carac_esp")
            
        
        return nombre
    
    def clean_apellido(self):
        apellido = self.cleaned_data["apellido"]

        if not apellido.isalpha():
            raise forms.ValidationError("Por favor, ingrese solo caracteres alfabéticos", code="carac_esp")
        
                    
        return apellido
    
    def clean_email(self):
        email = self.cleaned_data["email"]
        dominios_permitidos = ['gmail.com', 'hotmail.com', 'yahoo.com']

        if not any(email.endswith(dominio) for dominio in dominios_permitidos):
           raise forms.ValidationError("Por favor, ingrese una dirección de e-mail válida", code="email_invalido")
        
        return email
        

In the clean_apellido() method I’m not retrieving anything from the nombre field. I just access the value of apellido from the form’s cleaned_data.

There are no field validations in my Turno model, but here is the code just in case:

from django.db import models

class Turno(models.Model):
    nombre = models.CharField(max_length=50)
    apellido = models.CharField(max_length=50)
    email = models.EmailField(max_length=30)
    fecha = models.DateField()
    hora = models.TimeField()

Ignore the fields fecha and hora. Those are linked to a date-time picker and are filled automatically when a user picks a date and time from it. Those are not giving me any problems.

Well, I don’t see anythin wrong here.

To try to debug, I would add a print statement showing the apellido value right before the ValidationError raised for apellido, to check why it is not considered alpha.

If the print statement does not show up, it would mean that something else adds the error for apellido.

Your suggestion made me realize something really stupid. I added a print statement to see the values of both the nombre and apellido fields, and it returned the values I inputted. They both were alphabetical values, but I forgot the fact that I was writing two names in them. For example, I was inputting the name “George Walter” in the name field. It IS an alphabetical value, but there is a space between the first and second name. That’s why it was throwing me the error.
I told you I was just a beginner at this! XD.

I want to thank you a lot for your help! It’s very rare to find someone so willing to give you a hand.

You’re welcome.

Just a side note: when requesting help, in order to get most accurate responses about some errors, you should always post what you really observed (logs, rendered error, traceback) and not something you modify afterward because this can make the error ununderstandable : here, the is-invalid case with Perez appellido was obiously not something real. Having posted a real case with George Walter, someone would probably have directly found that this is not alpha.