Check form errors if is_valid() method is False

Hi,

I’m having problems with the is_valid() method :

I have this form

from django import forms
from .models import Registers, Alunos


class RegisterForm(forms.ModelForm):

    regalunoid = forms.ModelChoiceField(queryset=Alunos.objects.all(), label="Aluno:",
                                        widget=forms.Select(attrs={'disabled': 'disabled'}))

    lesson_at = forms.DateField(widget=forms.DateInput(attrs={"type": "date"}))

    class Meta:
        model = Registers
        fields = ["lesson_at", "regalunoid", "status"]
        labels = ["Data", "Aluno", "State"]
        exclude = ["regclasseid"]

This is the view

def RegisterUpdate(request, **kwargs):

    t = Turmas.objects.get(id=kwargs['pk'])

    RegisterFormSet = modelformset_factory(Registers, form=RegisterForm, extra=0)
    form = RegisterFormSet(queryset=Registers.objects.filter(regclasseid=kwargs['pk']))

    if request.method == 'POST':

        for name in request.POST:
            print("{}: {}".format(name, request.POST.getlist(name)))

        if form.is_valid():
            form = RegisterFormSet(request.POST)
            form.save()
            return render(request, 'schooladmin/turmas.html')
        else:
            print("failed to validate form")

    return render(request, 'schooladmin/registers.html', {'form': form, 'turma': t})

After the Submit I get this output on the server console:

csrfmiddlewaretoken: ['CxNje0WqOWpsnCgmfXsgKoJmJM2CZ71L7rel4qAJTr5cPCpJDOwU2dRbQbWPSSBs']
form-TOTAL_FORMS: ['3']
form-INITIAL_FORMS: ['3']
form-MIN_NUM_FORMS: ['0']
form-MAX_NUM_FORMS: ['1000']
form-0-lesson_at: ['2023-11-28']
form-0-status: ['P']
form-0-id: ['1']
form-1-lesson_at: ['2023-11-28']
form-1-status: ['P']
form-1-id: ['2']
form-2-lesson_at: ['2023-11-28']
form-2-status: ['P']
form-2-id: ['3']
**failed to validate form**

So I can conclude that the instruction

if form.is:valid()

is not True.

Where and how can I check the form errors or how can I fix this?

Thank in advance

The form is not valid because at the point that you are checking it, it has not yet been bound with data.
This line:

creates an empty formset.

You need to do this:

before the is_valid call.

After doing that, when I load the template I got these errors and I cannot see the Aluno.nome field

I put the form errors on top of the template and the message says
regalunoid This field is mandatory

What am I missing here?

If that’s how you’re submitting the form, then the issue is that you haven’t selected anything for the Aluno field in the form - you’re showing the default -------- selection for a field where a selection is required.

Or, more accurately, you’re saving a form instance that doesn’t have that field supplied. You either need to re-retrieve the data for that field, or update those instances instead of saving them.
(And when you make changes to your view here, please remember to post the updated view.)

I’ve made these changes to the view

def RegisterUpdate(request, **kwargs):

    t = Turmas.objects.get(id=kwargs['pk'])

    RegisterFormSet = modelformset_factory(Registers, form=RegisterForm, extra=0)
    form = RegisterFormSet(queryset=Registers.objects.filter(regclasseid=kwargs['pk']))

    if request.method == 'POST':

        for name in request.POST:
            print("{}: {}".format(name, request.POST.getlist(name)))

        f = RegisterFormSet(request.POST)

        if f.is_valid():
            f.save()
            return render(request, 'schooladmin/turmas.html')
        else:
            print("failed to validate form")

    return render(request, 'schooladmin/registers.html', {'form': form, 'turma': t})

With this change I can see the form with all the fields

But when I submit the form nothing is updated and the console shows the same QueryDict

csrfmiddlewaretoken: ['t62Rl31JYWQko2H0Xh3jNKBqMxkwsCn9Y0tTbtF23rw4Q2Qnl87X5zJfTWeJlnXQ']
form-TOTAL_FORMS: ['3']
form-INITIAL_FORMS: ['3']
form-MIN_NUM_FORMS: ['0']
form-MAX_NUM_FORMS: ['1000']
form-0-lesson_at: ['2023-11-28']
form-0-status: ['P']
form-0-id: ['1']
form-1-lesson_at: ['2023-11-28']
form-1-status: ['P']
form-1-id: ['2']
form-2-lesson_at: ['2023-11-28']
form-2-status: ['P']
form-2-id: ['3']
failed to validate form

with the bottom message that it did not validate the form, but there are no errors, apparently…

Correct. There’s no error in the form itself. You’re supplying everything that the form needs.

However, when you do the form.save(), it’s trying to save the models to the database, but what you’re supplying is not a complete model. You’ll need to initialize the formset with the queryset for the post in the same way you do it for the get.

Do you mean I have to put the form fields exactly in the same order as they are in the model?

class Registers(models.Model):

    class Meta:
        verbose_name_plural = 'Registers'

    lesson_at = models.DateField(help_text="Data da Aula:", verbose_name="Data da Aula:")
    regclasseid = models.ForeignKey(Turmas, on_delete=models.CASCADE, verbose_name="Classe:")
    regalunoid = models.ForeignKey(Alunos, on_delete=models.CASCADE, help_text="Aluno:", verbose_name="Aluno:")

    PRESENT_ABSENT = (
        ('P', 'Present'),
        ('A', 'Absent'),
    )

    status = models.CharField(max_length=1, choices=PRESENT_ABSENT, verbose_name="Presence/Absence:")

    def __int__(self):
        return self.Alunos.nome

No. The order that fields are defined anywhere is not important.

You’re creating an instance of the formset at:
f = RegisterFormSet(request.POST)

In this version for which you are accepting the POST, you also need to assign values to the fields that are not included in the form. (Disabled fields are not submitted as you see from the POST data you’ve printed.)

See the docs at Creating forms from models | Django documentation | Django.

You’ll save the formset with commit=False, then iterate through the formset. For each instance of the form, you’ll get the instance matching the id from the database, update that instance with what’s submitted from the form, and then save that instance.

<conjecture> Since you’re not submitting and saving a complete instance of the model, you might be better off changing this from being a ModelFormset to a regular formset. Or, there may be benefit to making this an inlineformset where the base class is the class that regclasseid is referring to. Without seeing the models, it’s difficult to tell what the best approach may be.</conjecture>

I’ve made the changes you said but it steel doesn’t work. No errors at all but the form is not validated because I can see my message "failed to validate form" which is the else instruction for the if f.is_valid()

The view:

def RegisterUpdate(request, **kwargs):

    t = Turmas.objects.get(id=kwargs['pk'])

    RegisterFormSet = modelformset_factory(Registers, form=RegisterForm, extra=0)
    form = RegisterFormSet(queryset=Registers.objects.filter(regclasseid=kwargs['pk']))

    if request.method == 'POST':

        for name in request.POST:
            print("{}: {}".format(name, request.POST.getlist(name)))

        f = RegisterFormSet(request.POST)

        if f.is_valid():
            instances = f.save(commit=False)
            for instance in instances:
                instance.regalunoid = f.cleaned_data['id']
            f.save()
            return render(request, 'schooladmin/turmas.html')
        else:
            print("failed to validate form")

    return render(request, 'schooladmin/registers.html', {'form': form, 'turma': t})

The server console responses

csrfmiddlewaretoken: ['08eTVZFymkrXojWdo0XTVDqwdF3qdB8Ep0QNAB3fXvbeplg7BnIp2SFlBNQadf2s']
form-TOTAL_FORMS: ['3']
form-INITIAL_FORMS: ['3']
form-MIN_NUM_FORMS: ['0']
form-MAX_NUM_FORMS: ['1000']
form-0-lesson_at: ['2023-11-29']
form-0-status: ['A']
form-0-id: ['1']
form-1-lesson_at: ['2023-11-29']
form-1-status: ['A']
form-1-id: ['2']
form-2-lesson_at: ['2023-11-29']
form-2-status: ['A']
form-2-id: ['3']
failed to validate form

Ok, it seems to me then that you are going to need to take one of the alternative approaches.

I think the easiest thing to do here is to convert this to an inline formset where you use Tumas as the base class, and the instances of Register from which to create the forms are those instances related to a specific Tumas. (This is the intent that I believe you’re going after by using the queryset parameter on the RegisterFormSet definition.)

See the example at Using an inline formset in a view.

Side note: You could probably get around this specific issue by setting required=False on the form field, but I think you’re really going to be better off doing it this way. Also, your code for trying to implement what I suggested isn’t correct and isn’t going to work the way you have it written - that section would need to be fixed.

I can’t make it work!!!

If I leave the Modelform with fields = '__all__' it works with no problem but if I change the list of fields the form won’t validate and there are no errors.

The inline option doesn’t work because I have 3 data models.

Why is this so difficult to achieve? Is it not possible to update a table with only some fields?

I will explain again what I am looking for.

I have 3 models.

Alunos (Students)
Turmas (Classes)
Registers (Register present or absent)

I want a form with the data that’s on the Registers model for a specific Turmas (Classe) then update the date lesson_atin just one form field and replicate it to all the related objects. I also need to update the status field to Presence or Absent.

Here is the code:

class Alunos(models.Model):

    class Meta:
        verbose_name_plural = 'Alunos'

    nome = models.CharField(max_length=50, help_text="Nome do aluno", verbose_name="Nome:")
    dtnasc = models.DateField(help_text="Data de Nascimento", verbose_name="Data de Nascimento:")
    alunoid = models.CharField(max_length=4, help_text="ID interno do aluno", verbose_name="ID Aluno:")
    classeid = models.ForeignKey(Turmas, on_delete=models.CASCADE, verbose_name="Classe:")
    educaid = models.ForeignKey(Educa, on_delete=models.CASCADE, verbose_name="Encarregado de Educação:")
    moradaid = models.ForeignKey(Morada, on_delete=models.CASCADE, verbose_name="Morada:")
    cpostalid = models.ForeignKey(CodPostal, on_delete=models.CASCADE, verbose_name="Código Postal:")
    localid = models.ForeignKey(Local, on_delete=models.CASCADE, verbose_name="Localidade:")
    nif = models.CharField(max_length=9, help_text="NIF do aluno ou encarregado", verbose_name="NIF:")
    ativo = models.BooleanField(default=True)
    notas = models.TextField(help_text="Notas importantes")

    def __str__(self):
        return self.nome
class Turmas(models.Model):

    class Meta:
        verbose_name_plural = 'Turmas'
        ordering = ['seq']

    seq = models.IntegerField(help_text="Número para ordenação:", verbose_name="Sequencia:")
    desc = models.CharField(max_length=50, help_text="Description:", verbose_name="Description:")
    turmaid = models.ForeignKey(Niveis, on_delete=models.CASCADE, help_text="Nome do Nível", verbose_name="Nível:")
    weekdays = models.CharField(max_length=7, choices=weekdays_choices, help_text="Dias da classe", verbose_name="Dias:")
    times = models.CharField(max_length=20, choices=class_times, help_text="Horário da classe", verbose_name="Horas:")
    profid = models.ForeignKey(Professores, on_delete=models.CASCADE, verbose_name="Professor:")
    anoid = models.ForeignKey(Ano, on_delete=models.CASCADE, verbose_name="Ano:")

    def __str__(self):
        return self.desc
class Registers(models.Model):

    class Meta:
        verbose_name_plural = 'Registers'

    lesson_at = models.DateField(help_text="Data da Aula:", verbose_name="Data da Aula:")
    regclasseid = models.ForeignKey(Turmas, on_delete=models.CASCADE, verbose_name="Classe:")
    regalunoid = models.ForeignKey(Alunos, on_delete=models.CASCADE, help_text="Aluno:", verbose_name="Aluno:")

    PRESENT_ABSENT = (
        ('P', 'Present'),
        ('A', 'Absent'),
    )

    status = models.CharField(max_length=1, choices=PRESENT_ABSENT, verbose_name="Presence/Absence:")

    def __int__(self):
        return self.Alunos.nome

How can I do this?

Any help would be much appreciated.

I don’t understand why you are saying this. There is nothing that I’ve seen posted here where the third model is at all relevant to what you’re trying to achieve, especially since you’re disabling the field.

Put it this way:

I have a many-to-many model with extra data and I want to display all the data in it but only allow certain fields to be updated, that’s why I’m using the disabled attroibute on the regalunoid field.

I need the user to see lesson_at(date), nome and status from Registers model and be able to update only the lesson_at(date) and status.

Basically if the field regalunoid is using the disabled attribute the form is not validated.

How can I overcome that?

Thanks

Yes.

Create your formset as an inline formset, where Turmas is the parent class, and Registers is the model for which the forms are to be generated.

You should then be able to use the same instance parameter when you’re creating the formset for both the GET and POST operations as shown in the example at Using an inline formset in a view, along with using your custom form definition as you’ve done above.

Now it’s a big mess…

I’ve changed the view like this:

view.py

def RegisterUpdate(request, **kwargs):                                                    
    turmas = Turmas.objects.filter(id=kwargs['pk'])                                       
    RegisterInLineFormSet = inlineformset_factory(Turmas, Registers, form=RegisterForm)   
    if request.method == "POST":                                                          
        formset = RegisterInLineFormSet(request.POST, request.FILES, instance=turmas)     
        if formset.is_valid():                                                            
            formset.save()                                                                
            return render(request, 'schooladmin/turmas.html')                             
    else:                                                                                 
        formset = RegisterInLineFormSet(instance=turmas)                                  
        return render(request, 'schooladmin/registers.html', {"formset:", formset})       

and I’m getting this error message:

AttributeError at /registers/1/
'QuerySet' object has no attribute 'pk'
Request Method:	POST
Request URL:	http://localhost:8000/registers/1/
Django Version:	4.2.7
Exception Type:	AttributeError
Exception Value:	
'QuerySet' object has no attribute 'pk'
Exception Location:	/Users/demiguelfreitas/django_lu/venv/lib/python3.11/site-packages/django/forms/models.py, line 1091, in __init__
Raised during:	schooladmin.views.RegisterUpdate
Python Executable:	/Users/demiguelfreitas/django_lu/venv/bin/python
Python Version:	3.11.1
Python Path:	
['/Users/demiguelfreitas/django_lu',
 '/Users/demiguelfreitas/django_lu',
 '/Library/Frameworks/Python.framework/Versions/3.11/lib/python311.zip',
 '/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11',
 '/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/lib-dynload',
 '/Users/demiguelfreitas/django_lu/venv/lib/python3.11/site-packages']
Server time:	Tue, 05 Dec 2023 12:11:07 +0000

The instance parameter at:

requires a single instance of an object to be provided, not a queryset.

This means that back at:

You need to get the instance needed here.

Filter returns a queryset, get returns a single instance.

Hello @mfreitas64 can you please replace your code with below one, I have also copied it from somewhere.

from django.forms import modelformset_factory
from django.shortcuts import render, get_object_or_404
from .models import Turmas, Registers
from .forms import RegisterForm

def RegisterUpdate(request, **kwargs):
    t = get_object_or_404(Turmas, id=kwargs['pk'])

    RegisterFormSet = modelformset_factory(Registers, form=RegisterForm, extra=0)
    formset = RegisterFormSet(queryset=Registers.objects.filter(regclasseid=kwargs['pk']))

    if request.method == 'POST':
        formset = RegisterFormSet(request.POST)

        # Print POST data for debugging
        for name in request.POST:
            print("{}: {}".format(name, request.POST.getlist(name)))

        # Print formset errors for debugging
        if not formset.is_valid():
            print("Formset Errors:", formset.errors)

        if formset.is_valid():
            formset.save()
            return render(request, 'schooladmin/turmas.html')
        else:
            print("Failed to validate form")

    return render(request, 'schooladmin/registers.html', {'formset': formset, 'turma': t})

Confirm whether it is working or not ?
Thanks

1 Like

Hi,

I’ve made a few changes to the code, here they are:

def RegisterUpdate(request, **kwargs):                                                                         
                                                                                                               
    t = Turmas.objects.get(id=kwargs['pk'])                                                                    
                                                                                                               
    RegisterFormSet = modelformset_factory(Registers, form=RegisterForm, extra=0,                              
                                           fields=["lesson_at", "regalunoid", "status"])                       
    form = RegisterFormSet(                                                                                    
        queryset=Registers.objects.select_related('regalunoid', 'regclasseid').filter(regclasseid=kwargs['pk'])
    )                                                                                                          
                                                                                                               
    if request.method == 'POST':                                                                               
        f = RegisterFormSet(request.POST)                                                                      
        if f.is_valid():                                                                                       
            for name in request.POST:                                                                          
                print("{}: {}".format(name, request.POST.getlist(name)))                                       
            f.save()                                                                                           
            return render(request, 'schooladmin/turmas.html')                                                  
        else:                                                                                                  
            if not f.is_valid():                                                                               
                print("Formset Errors:", f.errors)                                                             
                print("failed to validate form")                                                               
    return render(request, 'schooladmin/registers.html', {'form': form, 'turma': t}) 

and I get these messages on the server console:

Formset Errors: [{'regalunoid': ['Este campo é obrigatório.']}, {'regalunoid': ['Este campo é obrigatório.']}, {'regalunoid': ['Este campo é obrigatório.']}]
failed to validate form

This is because the field regalunoid is set to disabled on the RegisterForm

class RegisterForm(forms.ModelForm):

    regalunoid = forms.ModelChoiceField(queryset=Alunos.objects.all(), label="Aluno:",
                                        widget=forms.Select(attrs={'disabled': 'disabled'}))

    lesson_at = forms.DateField(widget=forms.DateInput(attrs={"type": "date"}))

    class Meta:
        model = Registers
        fields = '__all__'

If I remove the line

regalunoid = forms.ModelChoiceField(queryset=Alunos.objects.all(), label="Aluno:",
                                        widget=forms.Select(attrs={'disabled': 'disabled'}))

from the ModelForm everything works fine.

In your form here, you also need to set required=False, to tell Django that this field does not need a value supplied.

After a long period of try’s and with your help I found the solution on a SO old post. Someone has shared a little bit of code that works and does what I was looking for. Here it is:

    def __init__(self, *args, **kwargs):
        super(RegisterForm, self).__init__(*args, **kwargs)
        instance = getattr(self, 'instance', None)
        if instance and instance.id:
            self.fields['regalunoid'].required = False
            self.fields['regalunoid'].widget.attrs['disabled'] = 'disabled'

    def clean_regalunoid(self):
        instance = getattr(self, 'instance', None)
        if instance:
            return instance.regalunoid
        else:
            return self.cleaned_data.get('regalunoid', None)

Anyway, thank you very much Ken and Gulshan212 for your help.