I am having trouble figuring out how to control field validation for ModelForms.
Here is my Model Form:
class UserForm(ModelForm):
class Meta:
model = User
fields = ('email', 'password')
Here is how I am rendering my form in the template:
<form method="post" action="">
{% csrf_token %}
{{ userform.as_p }}
<input type="submit" value="Submit" />
</form>
Here is my view:
def login_page(request):
if request.method == 'POST':
userform = UserForm(request.POST)
if userform.is_valid():
email = request.POST['email']
password = request.POST['password']
user = authenticate(username=email, password=password)
if user is not None:
login(request, user)
return redirect('/dashboard/')
else:
# How can I give the user feedback about authentication failute here. Right now it just reloads the form without any messages.
return render(request, "login.html", {"userform": userform})
else:
# Because .is_valid() is called, The userform will validate for email addresses and that a password input is present here.
return render(request, "login.html", {"userform": userform})
else:
userform = UserForm()
return render(request, "login.html", {"userform": userform})
From reading the docs, this seems like it should be as simple as passing my form some custom attribute; but since I am working with a ModelForm, I am unsure whether this is achieved in the same way.
Could anyone provide an example or tips on how best to control ModelForm field errors?
Thanks!
Very easy, you have multiple ways to display the error messages:
First one:
1- Override the clean method inside your form/modelform:
def clean(self):
# To keep the main validation and error messages
super(your_form_name, self).clean()
# Now it's time to add your custom validation
if len(self.cleaned_data['password']) < 10:
self._errors['password']='your password\'s length is too short'
# you may also use the below line to custom your error message, but it doesn't work with me for some reason
raise forms.ValidationError('Your password is too short')
Second One
By using django built in validators.
You can use the validators inside the models with the custom error message you want like this:
RE = re.compile('^[0-9]{10}$')
field_name = models.CharField('Name', default='your_name',validators=[RegexValidator(regex=RE, message="Inapproperiate Name!")],)
where validators can utilize multiple built-in validation rules that you can validate against.
please refer to this django documentation
also you can use validators inside your forms definition, but the error message inside validators will not work as it did with models in case of errors, to custom error message use the below third method.
Third Method:
skill = forms.CharField(
label = 'SSkill',
min_length = 2,
max_length = 12,
# message kwarg is only usable in models, yet,
#it doesn't spawn error here.
#validators=[RegexValidator(regex=NME, message="In-approperiate number!")],
required = True,
#Errors dictionary
error_messages={'required': 'Please enter your skill','min_length':'very short entry','invalid':'Improper format'},
#Similar to default you pass in model
initial = 'Extreme Coder'
)
Where, requied, min_length, max_length, along with others, are all fomr kw arguments, where the invalid will only be selected when validators a couple of lines above didn't match the criteria you selected inside validators.
Fourth Method
In this method, you will override your modelform's definition like this:
def __init__(self, *args, **kwargs):
super(Your_Form, self).__init__(*args, **kwargs)
self.fields['password'].error_messages['required'] = 'I require that you fill out this field'
Fifth Method:
I have seen some nice way to custom errors in one of the stackoverflow posts like this:
class Meta:
model = Customer
exclude = ('user',)
fields = ['birthday','name']
field_args = {
"name" : {
"error_messages" : {
"required" : "Please let us know what to call you!"
"max_length" : "Too short!!!"
}
}
}
reference to fifth method is: Brendel's Blog
May be there are other/better ways of doing things, but those are the most common, at least to me :-)
I hope this post helps.
The Django authentication app has a built in AuthenticationForm that you use. If you look at the source code, you can see their approach is to check the username and password inside the clean method. That way, a validation error can be raised if the username and password combination is invalid, and the form will display the error.
You can import the AuthenticationForm and use it in your login view with the following import:
from django.contrib.auth.forms import Authentication
You may also be able to use the built in login view as well. This means you can take advantage of the added functionality, such as displaying a message if the user does not have cookies enabled. It also decreases the chance of you creating a security vulnerability.
If you want to return error or warning messages back to the user (even if there are no errors in the validated form) you can either use the messaging framework, or you can insert custom error messsages into the form.
I think the messaging framework is your best bet - I don't think it's a good idea to inject errors into the form. If someone has entered a wrong username and pword, it's not a validation error, so it shouldn't be treated as such. A validation error should only be thrown if the wrong type of data is entered to a field, not the incorrect value.
Related
I am working on a webapp where user can be a member of one (and only one) organisation - this is done with a foreignkey in the Profile model, which in turn has a one-to-one link with the default django.auth.user model. We also want to make sure that each email address is only used once within each organisation. To do this we added the following function to the Profile model:
def clean(self):
if self.organisation and Profile.objects.filter(
user__email=self.user.email,
organisation_id=self.organisation.id
).exists():
raise ValidationError({'user': _('The email address from this user is already used within this organisation!')})
return super(Profile, self).clean()
However, when I add a user through the Django admin using an duplicate email address all that gets displayed is a generic please fix the errors below message at the top of the form. No text is displayed near the email field and the ValidationError text isn't displayed at all - thus giving the admins no information on what actually went wrong.
Does anyone know why the ValidationError message isnt being displayed in the admin, and what steps we can take to rectify this?
We are using a standard ModelAdmin class
class ProfileAdmin(ModelAdmin):
def username(self, profile, **kwargs):
return u'{} ({})'.format(
profile.user.profile.full_name(),
profile.user.username)
search_fields = ['user__username', 'user__first_name', 'user__last_name', 'user__email']
list_display = ('username', 'organisation')
list_filter = ('organisation')
I think form validation is a good idea in these type of situations.
forms.py
class YourForm(forms.ModelForm):
def clean(self):
super(YourForm, self).clean()
data1 = self.cleaned_data.get('data1')
data2 = self.cleaned_data.get('data2')
# Add validation condition here
# if validation error happened you can raise the error
# and attach the error message with the field you want.
self.add_error('field_name', 'error message')
In admin.py
class YourAdminClass(admin.ModelAdmin):
form = YourForm
Raise ValidationError from ProfileAdmin class. For example, from clean_<field name> method.
Alright, so I have a form set up like so:
class LeadForm(forms.ModelForm):
class Meta:
model = Lead
fields = ['email']
In my views, I want to get the text the users puts into the field before validating it.
def index(request):
if request.method == 'POST':
lead_form = LeadForm(request.POST)
// This is the problem part
email = str(lead_form.fields['email'])
print email
if Lead.objects.get(email=email).exists():
return HttpResponse('Already signed up')
if lead_form.is_valid():
// Do something
else:
lead_form = LeadForm()
return render(request, 'myapp/index.html', {
'lead_form' : lead_form,
})
As you can see, I am trying to get the text from the email field and before validating, I want to check something. When I print the email, I get this in my terminal:
<django.forms.fields.EmailField object at 0x109c35d90>
What I want is the text however, as I am trying to see if a lead with that email exists or not. How can I do this? Any help will be appreciated!
This is validation. It should be done in the form; you can define a clean_email method on LeadForm to do it.
To answer your actual question though, you can access request.POST['email'] directly if you want. But don't do that; do it in the form.
Try like this,
email=request.POST.get('email')
print email
My django form has errors in the initial page load, before the form even has a chance to be submitted.
My view:
def example_function(request):
if request.method == 'POST':
# the request is GET
else:
form = MyForm(user=request.user)
import pdb;pdb.set_trace()
return render_to_response('templates/example.html', locals(), context_instance=RequestContext(request),)
Where I have my pdb imported, in the console I can see that my form already has errors. The output of form.errors in my console is all the fields in the model which are set to not null.
(Pdb) form.errors
{'example_field_1': [u'This field is required.'], 'example_field_2': [u'This field is required.']}
The form has not submit yet, but I am still getting errors. Can someone explain?
I'm using django 1.4.
My form:
class MyForm(forms.ModelForm):
captcha = ReCaptchaField()
_readonly_template = form.TextInput(attrs={'readonly':'readonly'})
first_name = forms.CharField(widget = _readonly_tempalte)
def __init__(self, data=None, *args, **kwargs):
data = data or {}
if 'user' in kwargs:
user = kwargs['user']
del kwargs['user']
data.update({
'first_name' : user.first_name,
})
super(MyForm, self).__init__(data, *args, **kwargs)
class Meta:
model = MyModel
My model:
class MyModel(models.Model):
first_name = models.CharField(max_length=255)
example_field_1 = models.CharField(max_length=255)
example_field_2 = models.CharField(max_length=255)
https://docs.djangoproject.com/en/1.8/ref/forms/validation/
accessing the form.errors attribute will trigger the various form validation methods. Those errors shouldn't show up when you render the form.
I'm not sure how the user field is structured, but keep in mind that if you want the user name, you may want to change that from request.user to request.user.username.
I hope you resolved your issue, but in case you haven't, I had a similar issue which I was able to resolve by using "or None" when setting the form after checking if it is a POST (or GET) request.
In your case it looks like this may be a slightly different issue, but I wondered if this snippet might fix things up:
if request.method == "POST":
form = MyForm(request.POST or None)
# .. do stuff....
else: #.....this is a GET
data = {'user': request.user.username} #note this is changed to username
form = MyForm(data)
Not sure if still useful, but adding it here, as I just ran into this for my ChoiceField items within my form.
I was getting the same error messages, but eventually found out I had forgotten to ad 'or None' when initiating the form inside my view.
The initial code inside my view function that was displaying the error messages from the start:
form=FormName(request.POST)
I just added the 'or None' to it:
form=FormName(request.POST or None)
And all good after that.
Don't you need to do something like this
form = NameForm(request.POST)
Rather then attempting to use the user object to populate the form? Will the user object have an example_field_1 in it?
https://docs.djangoproject.com/en/1.8/topics/forms/
This is the normal behavior.
Some properties of fields are checked on client side. The error messages belong to the form, are part of the html but are not displayed until needed. It saves a client-server request.
Trying to create a form where you can sign up as a user, and add yourself to one or more categories. Getting an error while doing it:
TypeError at /users/add-user/
'categories' is an invalid keyword argument for this function
Here's my forms.py:
class AddUser(forms.Form):
name = forms.CharField()
title = forms.CharField()
website = forms.CharField(required=False)
email = forms.EmailField()
phone = forms.CharField(required=False)
company = forms.ModelChoiceField(queryset=Company.objects.all())
categories = forms.ModelMultipleChoiceField(queryset=Category.objects.all())
The last line is the one I'm having trouble with.
Here's my views.py:
def add_user(request):
if request.method == 'POST':
form = AddUser(request.POST)
if form.is_valid():
cd = form.cleaned_data
try:
p = User.objects.get(email=cd['email'])
error = "There's already a user with that e-mail adress registered. Maybe he/she is already here?"
return render_to_response('users/add_user.html', {'form': form, 'error': error}, context_instance=RequestContext(request))
except User.DoesNotExist:
p = User(name=cd['name'], title=cd['title'], website=cd['website'], email=cd['email'], phone=cd['phone'], company=cd['company'], categories=cd['categories'])
p.save()
return HttpResponseRedirect('../thanks/')
else:
form = AddUser(request.POST)
error = "You can't really submit empty forms. Try adding something useful :)"
return render_to_response('users/add_user.html', {'form': form}, context_instance=RequestContext(request))
If anyone has any suggestions to the problem (or even suggestions in general to improve my code), I'd be glad! I'm a beginner to Django and all help is appreciated.
Your problem lies here:
p = User(name=cd['name'], title=cd['title'], website=cd['website'], email=cd['email'], phone=cd['phone'], company=cd['company'], categories=cd['categories'])
The problem is that the User model doesn't include a categories field, just as it doesn't include a website or company. See the list of the available fields.
There's different approaches to handling additional data in combination with Django's auth system. Sublcassing the User class or adding a model with additional info and a one-to-one field to the User come to mind. The latter option seems to be suggested, so I'd suggest going down that path.
A bit of a nit pick, but you did ask for other suggestions. This bit:
else:
form = AddUser(request.POST)
should (IMO) be changed to this:
else:
form = AddUser()
There's no need to use request.POST for a GET request. I've never tried that but I'm guessing it will work, you just get an empty set. So not an error but possibly a source of confusion.
Your error message is also not being used. A GET on the page is not an error at all in this case, it is just how the page is initially displayed.
I created a view which returns a form including a contact form and two phone_number forms, following this example:
multiple forms
The phone number forms should only be validated if the user inserts at least a value for one field in a phone number form. For example: a phone number has a type and a number. If the user is selecting the type, the number is required.
Now I'm wondering how i can check in the view whether the user inserted a value / selected a type or inserted a number. It should work like in the admin for inline editing a model.
my view looks like this:
def contact_add(request):
user = request.user
if request.method == 'POST':
cform = ContactForm(request.POST)
pforms = [PhoneNumberForm(request.POST, prefix=str(x)) for x in range(0,3)]
if cform.is_valid() and all([pf.is_valid() for pf in pforms]):
new_contact = cform.save(commit=False)
new_contact.created_by = user
new_contact.save()
for pf in pforms:
new_phone_number = pf.save(commit=False)
new_phone_number.contact = new_contact
new_phone_number.save()
request.user.message_set.create(message='Contact %s has been added.' % new_contact.__str__())
return HttpResponseRedirect("/crm/contacts/?oby=1")
else:
cform = ContactForm()
pforms = [PhoneNumberForm(prefix=str(x)) for x in range(0,3)]
return render_to_response(
'crm/contact_add.html',
{'cform': cform, 'pforms': pforms,},
context_instance = RequestContext(request),
)
Edit after first response below:
I tried to accomplish this task with custom validation but did not come to a satisfying end. To ease my task I changed the use-case a bit. I create a form which includes one Contact Form and one Address Form. The Address Form should only be validated if at least one field of the Address Form is filled in, since it should be possible to create a contact without creating a corresponding Address.
First I tried to use custome validation, which looked like this:
class AddressForm(forms.ModelForm):
class Meta:
model = Address
exclude = ('contact',)
def clean(self):
cleaned_data = self.cleaned_data
street = cleaned_data.get("street")
postal_code = cleaned_data.get("postal_code")
city = cleaned_data.get("city")
country = cleaned_data.get("country")
if not street and not postal_code and not city and not country:
#searching a better idea here
return 0
else:
return cleaned_data
But this does not really help, since this way I do not get rid of the validation errors.
This lead me to the idea that the clean method is the wrong place to do this validation, I think I have to check already in the POST.request whether all values for the Address Form are missing. And if they are missing, I do not call is_valid() for the Address Form and just ignore it. If at least one value is available, I just do the normal validation of the Address Form, without overriding the clean() method..
Good or bad idea?
If it is a good idea, how can I easily check the POST request for the values of my Address Form.
Probably I`m thinking way to complicated :-)
Edit: The solution using FormSets:
#login_required
def contact_add(request):
user = request.user
if request.method == 'POST':
cform = ContactForm(request.POST)
phonenumberformset = PhoneNumberFormSet(request.POST)
if cform.is_valid() and classificationformset.is_valid() and addressformset.is_valid() and phonenumberformset.is_valid():
new_contact = cform.save(commit=False)
new_contact.created_by = user
new_contact.save()
new_phonenumber_instances = phonenumberformset.save(commit=False)
for new_phonenumber in new_phonenumber_instances:
new_phonenumber.contact = new_contact
new_phonenumber.save()
request.user.message_set.create(message='Contact %s has been added.' % new_contact.__str__())
return HttpResponseRedirect("/crm/contacts/?oby=1")
else:
cform = ContactForm()
#By default, when you create a formset from a model, the formset will use
#a queryset that includes all objects in the model (e.g., Author.objects.all()).
#Here we want to present an empty formset in order to add a new object
phonenumberformset = PhoneNumberFormSet(queryset=PhoneNumber.objects.none())
return render_to_response(
'crm/contact_add.html',
{'cform': cform, 'phonenumberformset': phonenumberformset,},
context_instance = RequestContext(request),
)
Please note that this can also be accomplished using an inlineformset_factory, see my other post for more details: link
Note that if you are using FormSets you have to include a management_form for each form_set in your template. docs
Otherwise you get this error:
[u'ManagementForm data is missing or has been tampered with']
Using a formset inside a view is as easy as using a regular Form class. The only thing you will want to be aware of is making sure to use the management form inside the template.
{{ context.phonenumberformset.management_form }}
You should be using formsets rather than messing around with dynamic prefixes for your PhoneNumber subform - it will make everything much easier, and this is indeed how the admin manages inline forms (see also the model formsets documentation).
Formsets are intelligent enough that if no information is entered in one form of the formset, it does not enforce the required elements - but if one element is filled, it will enforce all the validation requirements. This sounds like it should solve your problem.
What you want to do is define custom validation on the form.
class PhoneNumberForm(forms.Form):
# Everything as before.
...
def clean(self):
cleaned_data = self.cleaned_data
phone1 = cleaned_data.get("phone1")
if phone1:
# validate manually, and if it doesn't pass:
self._errors["phone1"] = ErrorList(["Hey, this field is wrong."])
del cleaned_data["phone1"]
# Always return the full collection of cleaned data.
return cleaned_data
Then in the view, you want to rely on Django's built-in error form validation error handling:
{{ pforms.phone1 }}
{{ pforms.phone1.errors }}