Django Form Validation on Class Based View
Asked Answered
B

4

6

I have a very simple Class Based View:

In views.py:

class IncidentEdit(UpdateView):
    model=Incident
    fields = visible_field_list
    sucess_url = '/status'

works fine as-is. I have associated CreateView, DeleteView, etc. I can create edit and delete records. Now to fine-tune the project, I need to add field validation.

My question: Where do I put basic validation code when I have based the view on the 'model=' rather than 'form='?

I could change everything to use form based views, but the whole idea was to keep it simple and it works, I just don't have the form validation except for basic 'Field Required' type validation that was defined in the model declaration.

For example, I need to make sure that one field equals the sum of two other fields. Like,

ClassRoomTotal = NumBoys + NumGirls

and raise a validation error for the ClassRoomTotal field if the sum doesn't match the total.

Thanks in advance.
I know it is a simple answer.

Suggestions like, "You can't do that, you have to use form=IncidentForm and define a form class." would help.

Barny answered 1/5, 2015 at 4:41 Comment(0)
S
10

Well,

you can't do that, you have to use form = IncidentForm

or at least it is the simplest solution.

Note that you have to use form_class = IncidentForm, not form = IncidentForm and keep model = Incident.

I don't see using ModelForm as something that will increase your project's complexity, this is exactly their use case. Doing it another way would be making things complex.

It can be as simple as:

class IncidentForm(ModelForm):
    class Meta:
        model = Incident
        # Define fields you want here, it is best practice not to use '__all__'
        fields = [...]

    def clean(self):
        cleaned_data = super(IncidentForm, self).clean()

        field_1 = cleaned_data.get('field_1')
        field_2 = cleaned_data.get('field_2')
        field_3 = cleaned_data.get('field_3')

        # Values may be None if the fields did not pass previous validations.
        if field_1 is not None and field_2 is not None and field_3 is not None:
            # If fields have values, perform validation:
            if not field_3 == field_1 + field_2:
                # Use None as the first parameter to make it a non-field error.
                # If you feel is related to a field, use this field's name.
                self.add_error(None, ValidationError('field_3 must be equal to the sum of field_1 and filed_2'))

        # Required only if Django version < 1.7 :
        return cleaned_data


class IncidentEdit(UpdateView):
    model = Incident
    form_class = IncidentForm
    fields = visible_field_list
    success_url = '/status'
Shon answered 1/5, 2015 at 7:19 Comment(3)
Your example looks very promising. I have created a ModelForm in my forms.py file following this example. When switching from "model=Incident" to "form=IncidentForm" in the view, I get the following error: "IncidentEdit is missing a QuerySet. Define IncidentEdit.model, IncidentEdit.queryset, or override IncidentEdit.get_queryset()." But if I add BOTH model=Incident and form=IncidentForm, it behaves the same as it did before I added form=IncidentForm. Given my original four line class based view... what should it look like to use the modelform that I defined?Barny
You need to use form_class = IncidentForm and keep model = Incident.Shon
OK... Thanks to aumo... it works. For other Newbies out there: On the view, I needed to add form_class=IncidentForm NOT form= ... and you need the model=Incident too... It uses that line to retrieve the item to be edited and the form_class to deal with form validation. Also in the forms.py file I needed to add "from django.core.exceptions import ValidationError" otherwise django says "ValidationError is not defined." Thank you aumo for your answer... I tried to read the docs, which left me confused, but your example helped me fix it and get back to coding.Barny
S
17
class IncidentEdit(UpdateView):

    ...

    def form_valid(self, form):
        if form.cleaned_data['email'] in \
        [i.email for i in Incident.objects.exclude(id=get_object().id)]:
            # Assume incident have email and it should be unique !!
            form.add_error('email', 'Incident with this email already exist')
            return self.form_invalid(form)
        return super(IncidentEdit, self).form_valid(form)

Also, hope this link would be useful. http://ccbv.co.uk/projects/Django/1.7/django.views.generic.edit/UpdateView/

Sgraffito answered 28/7, 2015 at 13:44 Comment(2)
This really helped. Just wanted to ask that will this method be less effective in any way when compared to declaring your own form; giving a form_class?Lemur
@ShivamSharma: I never faced a lack of effectiveness with this approach. Nor I see any performance bottle neck. If you find any please mention, I'd like to look for solutions. In my example, replacing list comprehension with values_list queryset method would be a good idea. When you have several validations to be done, it might look cleaner to write the form.Sgraffito
S
10

Well,

you can't do that, you have to use form = IncidentForm

or at least it is the simplest solution.

Note that you have to use form_class = IncidentForm, not form = IncidentForm and keep model = Incident.

I don't see using ModelForm as something that will increase your project's complexity, this is exactly their use case. Doing it another way would be making things complex.

It can be as simple as:

class IncidentForm(ModelForm):
    class Meta:
        model = Incident
        # Define fields you want here, it is best practice not to use '__all__'
        fields = [...]

    def clean(self):
        cleaned_data = super(IncidentForm, self).clean()

        field_1 = cleaned_data.get('field_1')
        field_2 = cleaned_data.get('field_2')
        field_3 = cleaned_data.get('field_3')

        # Values may be None if the fields did not pass previous validations.
        if field_1 is not None and field_2 is not None and field_3 is not None:
            # If fields have values, perform validation:
            if not field_3 == field_1 + field_2:
                # Use None as the first parameter to make it a non-field error.
                # If you feel is related to a field, use this field's name.
                self.add_error(None, ValidationError('field_3 must be equal to the sum of field_1 and filed_2'))

        # Required only if Django version < 1.7 :
        return cleaned_data


class IncidentEdit(UpdateView):
    model = Incident
    form_class = IncidentForm
    fields = visible_field_list
    success_url = '/status'
Shon answered 1/5, 2015 at 7:19 Comment(3)
Your example looks very promising. I have created a ModelForm in my forms.py file following this example. When switching from "model=Incident" to "form=IncidentForm" in the view, I get the following error: "IncidentEdit is missing a QuerySet. Define IncidentEdit.model, IncidentEdit.queryset, or override IncidentEdit.get_queryset()." But if I add BOTH model=Incident and form=IncidentForm, it behaves the same as it did before I added form=IncidentForm. Given my original four line class based view... what should it look like to use the modelform that I defined?Barny
You need to use form_class = IncidentForm and keep model = Incident.Shon
OK... Thanks to aumo... it works. For other Newbies out there: On the view, I needed to add form_class=IncidentForm NOT form= ... and you need the model=Incident too... It uses that line to retrieve the item to be edited and the form_class to deal with form validation. Also in the forms.py file I needed to add "from django.core.exceptions import ValidationError" otherwise django says "ValidationError is not defined." Thank you aumo for your answer... I tried to read the docs, which left me confused, but your example helped me fix it and get back to coding.Barny
G
0

The same problem confuse me, many thanks to aumo and Vinayak for theirs answers inspired me so much!

As a beginner, I'm always try to use the "Model + Class Based View + Templates" structure directly to avoid my app getting out of control.

Same behavior with overriding form_valid function in CBV (answered by Vinayak), enclose the function by customizing Mixin class might look better. My code asf(base on django version 2.0):

# models.py   
class Incident(models.Model):
    numboys = models.SmallIntegerField(default=0)
    numgirls = models.SmallIntegerField(default=0)
    classttl = models.SmallIntegerField(default=0)


# views.py
def retunTestPassedResp(request):
    return HttpResponse()

class NumValidationMixin:
    def form_valid(self, form):
        data = self.request.POST
        boys = data.get('numboys')
        girls = data.get('numgirls')
        ttl = data.get('classttl')
        if boys and girls and ttl:
            if int(ttl) == int(boys) + int(girls):
                return super().form_valid(form)
            # use form.errors to add the error msg as a dictonary
            form.errors['input invalid'] = '%s + %s not equal %s'%(boys, girls, ttl)
        form.errors['input invalid'] = 'losing input with boys or other'
        return self.form_invalid(form)

class UpdateIncident(NumValidationMixin, UpdateView):
    model = Incident
    fields = ['numboys', 'numgirls', 'classttl']
    success_url = reverse_lazy('test-passed')

# templates/.../Incident_form.html
[...]
<body>
    {{form}}
    {% if form.errors %}
    <p>get error</p>
        {{form.errors}}        
    {% endif %}
</body>

I also make a unittest, and got passed.

# tests.py
class IncidentUpdateTest(TestCase):
    def setUp(self):
        Incident.objects.create()

    def test_can_update_with_right_data(self):
        [...]

    def test_invalid_error_with_illegal_post(self):
        response = self.client.post(
            reverse('update-incident', args=(1,)),
            data={'numboys': '1', 'numgirls': '1', 'classttl': '3'}
        )
        self.assertEqual(Incident.objects.first().classttl, 0)
        # testing response page showing error msg
        self.assertContains(response, 'not equal')

For more exact code example and explaining, pls find in django document.

I wish this answer will help those beginners and self-study friends who are like me.

Grisly answered 19/2, 2018 at 21:32 Comment(0)
D
0

I just stumbled on the question, and though it is quite an old one, I thought I would add my bit.

Sometime back the problem had bothered me somewhat and I used a straight forward solution (being described hereunder):

In forms.py suppose we have a form as this:

forms.py

class MyForm(forms.ModelForm):
    class Meta:
        model = MyModel
        fields = ('field_a', 'field_b')
        ...
        ...
    def clean(self):
        super(MyForm, self).clean()
# We do some kind of validation
        if self['field_a'].value() and self['field_b'].value() is not None or '': # Check for none zero or null values
            if self['field_a'].value() == self['field_b'].value(): # Check and raise error on some condition not being met, eg. here we don't want that the fields should have the same values
                raise forms.ValidationError('The field values can not be equal!!')

Next in the UpdateView we declare the form_class and the underlying model.

And finally in the template, display the non-field errors like this:

abc.html

...
...
{{ form.non_field_errors }}

In the event of validation failure, the intended massage is displayed on the form.

Relevant Django docs here.

Derm answered 27/3, 2022 at 14:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.