Django form.clean() run before field validators - django

https://docs.djangoproject.com/en/1.10/ref/forms/validation/
States that run_validators() is run before the form subclass’s clean().
My model looks like:
def validate_ascii(value):
try:
value.encode('ascii')
except UnicodeEncodeError:
raise ValidationError("Contains non-ascii characters")
class Keyword(models.Model):
name = models.CharField(max_length=50, unique=True, validators=[validate_ascii])
In my form's clean() method
class KeywordAdminForm(ModelForm):
class Meta:
model = Keyword
def clean(self):
import pdb; pdb.set_trace()
cleaned_data = super(KeywordAdminForm, self).clean()
import pdb; pdb.set_trace()
return super(KeywordAdminForm, self).clean()
After that, the validators for each field in the form is run. This is causing issues because my clean method assumes each field has had the validator run first and crashes.
Why is my form's clean() method being run before the validators on the field?

Change your forms clean() method to call cleaned_data = super(KeywordAdminForm, self).clean() first before performing the rest of your validation. This is how the docs recommend you do it
This section of the docs has an explanation for your issue.
Model validation (Model.full_clean()) is triggered from within the
form validation step, right after the form’s clean() method is called.
This would suggest that you cannot rely on any model validation in your clean method

According to "Validation on a ModelForm" paragraph of the docs:
There are two main steps involved in validating a ModelForm:
Validating the form
Validating the model instance
This defines that there are two completely different layers of validation, one in the model level and one in the form level.
Therefore it is clearly wrong to expect that these layers of validation are somehow related.
However, there is a sound solution as described in the "Overriding the Default Fields" paragraph of the same chapter:
If you want to specify a field’s validators, you can do so by defining
the field declaratively and setting its validators parameter.
Your example could become:
from django.forms import CharField, ModelForm
from myapp.models import Keyword
class KeywordAdminForm(ModelForm):
slug = CharField(max_length=50, validators=[validate_ascii])
class Meta:
model = Keyword
fields = '__all__'
Bear in mind though to read the green "Note" that follows this example which states that:
Similarly, fields defined declaratively do not draw their attributes
like max_length or required from the corresponding model. If you want
to maintain the behavior specified in the model, you must set the
relevant arguments explicitly when declaring the form field.
Alternatively, you can do something like this:
from django.forms import CharField, ModelForm
from myapp.models import Keyword, validate_ascii
class KeywordAdminForm(ModelForm):
def clean_slug(self):
slug = self.cleaned_data.get('slug')
validate_ascii(slug)
return slug
def clean(self):
cleaned_data = super().clean()
if self.errors:
return cleaned_data
...
return cleaned_data
class Meta:
model = Keyword
fields = '__all__'
The above code works because it potentially raises a ValidationError inside clean_<field>() that is called before clean().

Related

Django model forms validation in Django admin enters clean method when model validation has failed

I have a Django admin class defined like this:
class MyModelAdmin(admin.ModelAdmin):
form = MyForm
class MyForm(forms.ModelForm):
class Meta:
model = Task
fields = ('project', 'title', 'description', 'internal_assignees', 'task_list',
'additional_description', 'labels', 'skill_level', 'estimated_hours',
'merge_request_url', 'branch_name',)
def clean(self):
super().clean()
print(self.errors)
Assume my model has a required field, which the user does not submit when performing POST request. The expected behavior in this case is that Django will not enter my overriden clean method. But it actually does. And self.errors contain error message.
The expected behavior was that Django will not enter the clean method if any of the required fields have not been submitted and will raise Bad request. Do I understand something incorrectly ? If so, does it mean that I have to check whether required fields are not or not in my clean method?

How to overwrite clean and clean_fieldname for ModelFormMixin

I found this
How to properly overwrite clean() method
and this
Can `clean` and `clean_fieldname` methods be used together in Django ModelForm?
but it seems to work differently if one is using the generic class mixins ModelFormMixin.
My class is also derived from ProcessFormView.
Is def form_valid(self, form): the only point where I can overwrite the form handling process?
You are confusing views and forms. A CreateView for example makes use of a ModelForm to create an object. But instead of letting the view construct the ModelForm, you can specify such form yourself and then pass this as form_class to the view.
For example say you have a Category model with a name field, and you wish to validate that the name of the Category is all written in lowercase, you can define a ModelForm to do that:
from django import forms
from django.core.exceptions import ValidationError
class CategoryForm(forms.ModelForm):
def clean_name(self):
data = self.cleaned_data['recipients']
if not data.islower():
raise ValidationError('The name of the category should be written in lowercase')
return data
class Meta:
model = Category
fields = ['name']
now we can plug in that ModelForm as form for our CategoryCreateView:
from django.views.generic import CreateView
class CategoryCreateView(CreateView):
model = Category
form_class = CategoryForm
The validation thus should be done in the ModelForm, and you can then use that form in your CreateView, UpdateView, etc.

How to validate objects from reverse side of foreign key

I have two objects that are connected together by a ForeignKey.
class Question(models.Model):
text = models.Charfield()
class AnswerOption(models.Model):
text = models.Charfield()
question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name="options")
When Question is created in the admin interface I'm using a Inline form for AnswerOptions so that they can be created at the same time. I'd like to perform some validation on the Question and it needs access to the new AnswerOptions to make the decision.
I've added a clean method to Question but the options property is empty.
What is the correct way to validate Question?
[EDIT]
Made it clear that Question needs access to the AnswerOptions to be able to validate everything.
[EDIT]
Added explicit reference to using an InlineForm for AnswerOptions in the admin interface.
I'd do this through a Django form, which have a more robust interface for
validation. The clean method on your form is the place for this type
of validation.
# forms.py
from django import forms
from .models import Question
class QuestionForm(forms.Form):
text = models.Charfield()
class Meta:
model = Question
def clean(self):
options = self.cleaned_data['options']
if not option.are_ok:
raise forms.ValidationError
# admin.py
from django import admin
from .forms import QuestionForm
class QuestionAdmin(admin.ModelAdmin):
    form = QuestionForm
...
From the docs:
The form subclass’s clean() method can perform validation that requires access to multiple form fields. This is where you might put in checks such as “if field A is supplied, field B must contain a valid email address”. This method can return a completely different dictionary if it wishes, which will be used as the cleaned_data.
This is what I've discovered:
When creating Inline forms in the admin interface Django creates a Formset to handle the multiple forms. (The example here is the same as my use case)
Formsets have a clean() method like other forms and they have a forms property to access the child forms.
Just like normal forms they have an instance property that refers to the 'base' class and the individual forms have an instance property that gets you a instance of the newly submitted data.
Putting it all together:
# models.py
class Question(models.Model):
text = models.Charfield()
class AnswerOption(models.Model):
text = models.Charfield()
question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name="options")
# admin.py
from django.contrib import admin
from django.forms.models import BaseInlineFormSet
class AnswerOptionFormset(BaseInlineFormset):
def clean(self):
super().clean() # See note in docs about calling this to check unique constraints
#self.instance -> Question, with all the newly submitted, and validated, data.
#self.forms -> iterator over all the submitted AnswerOption forms
#for f in self.forms:
# f.instance -> instance of AnswerOption containing the new validated data
#Note: self.instance.options will refer to the previous AnswerOptions
#raise ValidationError for anything that is wrong.
#It is also possible to modify the data in self.instance or form.instance instead.
class AnswerOptionInline(admin.TabularInline):
formset = AnswerOptionFormset # note formset on AnswerOption NOT QuestionAdmin
class QuestionAdmin(admin.ModelAdmin):
inlines = [AnswerOptionInline]

Django: how to write code for modelform of user registration

I would want to make register form and check if the
user's email is a new one by checking the DB.
But the problem is that I don't know how to make it.
I tried using unique=True to Field and clean_email() method.
I have 2 questions.
1) When ValidationError happens at the step of unique=True,
how could I catch that error? should I override the clean_field() method
of Model? And How could I make it?
2) how could I write efficient and nice code for this registration app?
Below are the source code i made.
# models.py
from django.db import models
class User(models.Model):
university = models.ForeignKey('University')
mail = models.EmailField(unique=True)
password = models.CharField()
nickname = models.CharField()
class University(models.Model):
name = models.CharField()
mail = models.CharField()
from django.forms import ModelForm
class LoginForm(ModelForm):
class Meta:
model = User
fields = ('mail', 'password')
class RegisterForm(ModelForm):
class Meta:
model = User
def clean_mail():
data = self.cleaned_data['mail']
if User.objects.filter(mail=data).exists():
raise forms.ValidationError("This email is already exists")
# views.py
def register(request):
if request.method == 'POST':
###
if form.is_valid():
I am so confused to make this.
I am waiting for a help Thanks in advance ;)
You don't catch that error. Django does it for you: when you call form.is_valid(), it calls each of the clean_field methods and catches any ValidationError exceptions, putting them into the form.errors attribute.
So, what you have is already fine.
Except for the major, huge, absolutely vital issue that you should never store passwords in plain text. Don't create your own standalone user class: inherit from AbstractBaseUser and use Django's built-in methods to create users and hash their passwords. You must not do it like this.
First of All, Your clean_mail() should return self.cleaned_data['mail'] , if error is not raised. Although clean_mail is not required here
Secondly, Django will automatically check for uniqueness of the Email if you set unique=Ture in Model. An error would be raised if it is not unique.
You can over-ride unique error text by providing that in ModelForm's Meta options.
e.g.
class RegisterForm(ModelForm):
class Meta:
model = User
error_messages = {
'mail': {
'unique': _("This email already exists"),
},
}
Note: error_messages option has been added in django v1.6 only...
For previous versions:
You can over-ride the field mail in the ModelForm directly. e.g Define at top of the ModelForm
class RegisterForm(ModelForm):
mail = forms.EmailField(max_length=255, error_messages = { 'unique': 'xyz....',})
class Meta:.......
you can find reference here:https://docs.djangoproject.com/en/1.5/ref/forms/fields/#error-messages
Take a quick look at the following page: https://docs.djangoproject.com/en/dev/ref/models/instances/
The section "Validating objects" guides you through the steps in model validation.
If you require single-column uniqueness on User.mail, simply add the keyword unique=True to the Charfield. The stock implementation of validate_unique() will handle this.

'UpdateView with a ModelForm as form_class'-issue

The code I would like to get is for a page that has a simple form of one field to change a user's email address using an UpdateView.
Sounds simple, but the difficulty is that I want the URL mapping url(r'email/(?P<pk>\d+)/$', EmailView.as_view(),) not to use the id of the Model used in my ModelForm (User) but the id of another Model (Profile).
The id of a Profile instance of a specific user can be called as follows inside a view: self.user.get_profile().id. I am using the Profile model of the reusable app userena if you are wondering.
A (afaik not optimally implemented ¹) feature of an UpdateView is that if you want to use your own ModelForm instead of letting the UpdateView derive a form from a Model you need to(otherwise produces an Error) define either model, queryset or get_queryset.
So for my EmailView case I did the following:
forms.py
class EmailModelForm(forms.ModelForm):
class Meta:
model = User
fields = (
"email",
)
def save(self, *args, **kwargs):
print self.instance
# returns <Profile: Billy Bob's Profile> instead of <User: Billy Bob> !!!
return super(EmailModelForm, self).save(*args, **kwargs)
views.py
class EmailView(UpdateView):
model = Profile # Note that this is not the Model used in EmailModelForm!
form_class = EmailModelForm
template_name = 'email.html'
success_url = '/succes/'
I then went to /email/2/. That is the email form of the user that has a profile with id 2.
If I would run a debugger inside EmailView I get this:
>>> self.user.id
1
>>> profile = self.user.get_profile()
>>> profile.id
2
So far so good. But when I submit the form it won't save. I could overwrite the save method in the EmailModelForm but I'd rather override something in my EmailView. How can I do that?
¹ Because UpdateView could just derive the model class from the ModelForm passed to the form_class attribute in case it is a ModelForm.
Having your view and model form correspond to different models seems a bad idea to me.
I would set model = User in your EmailView, then override get_object so that it returns the user corresponding to the given profile id.