Django: ValidationError message not showing in admin changeform - django

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.

Related

Extending User model with one to one field- how to set user instance in view

I am trying to extend the user model using a one to one relationship to a UserProfile model. I added some boolean fields and in the view I am trying to use those fields as permissions.
Here is my model:
class UserProfile(models.Model):
user = models.OneToOneField(User)
FirstName = models.CharField(max_length=25)
LastName = models.CharField(max_length=25)
ProximityAccess = models.BooleanField(default=True)
NewProxAccess = models.BooleanField(default=False)
def __unicode__(self):
return self.user.username
and here is the view I am trying to use:
#login_required
def NewProx(request):
if UserProfile.NewProxAccess:
if request.method == 'POST':
form = ProxForm(request.POST)
if form.is_valid():
ProxPart_instance = form.save(commit=True)
ProxPart_instance.save()
return HttpResponseRedirect('/proximity')
else:
form = ProxForm()
return render(request, 'app/NewProx.html', {'form': form})
else:
raise PermissionDenied
I don't get any error messages but it does not work as intended. I was hoping that if the user profile had NewProxAccess set to False it would raise the PermissionDenied but it doesn't. I have the admin module wired up and I can select or deselect the checkbox for that field but it has no effect. If I comment out the rest I can get it to show the Permission Denied error so it has to be in the view (I think). I think I am missing a line the establishes the logged in user as the user instance so we can check to see if the user has the permission or not. I know there are a ton of ways to do this and there is probably a better way but for the sake of learning, what is it that I am missing for this to work?
Thanks
Scott
As you want to check access for particular profile but not UserProfile model you need to do:
if request.user.userprofile.NewProxAccess:
# your code
As a note: according to PEP8 best practices you should use camelCase only for naming Classes. For attrs, functions use underscore: my_function

Assigning a custom form error to a field in a modelform in django

I am trying to assign a custom form error to a field in a modelform in django so that it appears where a 'standard' error such as the field being left blank, with the same formatting (which is handled by crispy forms).
My model form clean method looks like this:
def clean(self):
cleaned_data = super(CreatorForm, self).clean()
try:
if cleaned_data['email'] != cleaned_data['re_email']:
raise forms.ValidationError({'email': "Your emails don't match!"})
except KeyError:
pass
return cleaned_data
And in my template I display the form/re-submitted form like this:
{{creator_form|crispy}}
I would like the error to appear below the re_email field if possible (though currently I thought I'd have better luck getting it below the email field. At the moment it appears at the top of the form, unformatted.
For the re_email field, despite not being part of the model, the error displayed for leaving it blank appears below the re_email field. How do I 'attach' errors to fields so they are displayed beneath/near them?
All help appreciated thanks
To get the error to display on a specific field you need to explicitly define what field the error goes on since you're overriding .clean(). Here is a sample taken from the Django docs:
class ContactForm(forms.Form):
# Everything as before.
...
def clean(self):
cleaned_data = super(ContactForm, self).clean()
cc_myself = cleaned_data.get("cc_myself")
subject = cleaned_data.get("subject")
if cc_myself and subject and "help" not in subject:
# We know these are not in self._errors now (see discussion
# below).
msg = u"Must put 'help' in subject when cc'ing yourself."
self._errors["cc_myself"] = self.error_class([msg])
self._errors["subject"] = self.error_class([msg])
# These fields are no longer valid. Remove them from the
# cleaned data.
del cleaned_data["cc_myself"]
del cleaned_data["subject"]
# Always return the full collection of cleaned data.
return cleaned_data

displaying errors with django ModelForm

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.

Custom validation in admin for list_editable fields

I have a admin form with custom validation. Some of the form fields are displayed in the list view via list_editable. When I modify these fields via the list view the custom validation does not kick in. It does work when I use the regular change form, though. So the question is how do I validate changes done via the "change_list" page.
The code might make it clearer
class ProjectForm(ModelForm):
class Meta:
model = Project
def clean(self):
print "validating!"
data = self.cleaned_data
if data.get('on_frontpage') and not data.get('frontpage_image'):
raise ValidationError('To put a project on the frontpage you must \
specify a "Frontpage image" first.')
return data
class ProjectAdmin(AdminImageMixin, DisplayableAdmin, SortableAdmin):
form = ProjectForm
...
list_editable = ("status", "on_frontpage",)
list_display = ("title", "status", "on_frontpage")
Thanks!
Found it. One can specify the form used on the "change_list" page by overriding "get_changelist_formset" method in ModelAdmin:
https://code.djangoproject.com/browser/django/trunk/django/contrib/admin/options.py#L524
Override the ModelAdmin.get_changelist_formset(request, **kwargs) method:
from django.forms import BaseModelFormSet
class MyAdminFormSet(BaseModelFormSet):
pass
class MyModelAdmin(admin.ModelAdmin):
def get_changelist_formset(self, request, **kwargs):
kwargs['formset'] = MyAdminFormSet
return super().get_changelist_formset(request, **kwargs)
For more details please check the Django admin site documentation.
I think #Jorge Barata's is the correct answer, thank you very much.
Please allow me to attach a success example here.
class MyAdminFormSet(BaseModelFormSet):
def clean(self):
form_set = self.cleaned_data
for form_data in form_set:
if form_data['field1'] != form_data['field2']:
raise forms.ValidationError(f'Item: {form_data["id"]} is not valid')
return form_set
Tested on Django 2.2

Can I count on the order of field validation in a Django form?

I have a Django form with a username and email field. I want to check the email isn't already in use by a user:
def clean_email(self):
email = self.cleaned_data["email"]
if User.objects.filter(email=email).count() != 0:
raise forms.ValidationError(_("Email not available."))
return email
This works, but raises some false negatives because the email might already be in the database for the user named in the form. I want to change to this:
def clean_email(self):
email = self.cleaned_data["email"]
username = self.cleaned_data["username"]
if User.objects.filter(email=email, username__ne=username).count() != 0:
raise forms.ValidationError(_("Email not available."))
return email
The Django docs say that all the validation for one field is done before moving onto the next field. If email is cleaned before username, then cleaned_data["username"] won't be available in clean_email. But the docs are unclear as to what order the fields are cleaned in. I declare username before email in the form, does that mean I'm safe in assuming that username is cleaned before email?
I could read the code, but I'm more interested in what the Django API is promising, and knowing that I'm safe even in future versions of Django.
Update
.keyOrder no longer works. I believe this should work instead:
from collections import OrderedDict
class MyForm(forms.ModelForm):
…
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
field_order = ['has_custom_name', 'name']
reordered_fields = OrderedDict()
for fld in field_order:
reordered_fields[fld] = self.fields[fld]
for fld, value in self.fields.items():
if fld not in reordered_fields:
reordered_fields[fld] = value
self.fields = reordered_fields
Previous Answer
There are things that can alter form order regardless of how you declare them in the form definition. One of them is if you're using a ModelForm, in which case unless you have both fields declared in fields under class Meta they are going to be in an unpredictable order.
Fortunately, there is a reliable solution.
You can control the field order in a form by setting self.fields.keyOrder.
Here's some sample code you can use:
class MyForm(forms.ModelForm):
has_custom_name = forms.BooleanField(label="Should it have a custom name?")
name = forms.CharField(required=False, label="Custom name")
class Meta:
model = Widget
fields = ['name', 'description', 'stretchiness', 'egginess']
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
ordered_fields = ['has_custom_name', 'name']
self.fields.keyOrder = ordered_fields + [k for k in self.fields.keys() if k not in ordered_fields]
def clean_name(self):
data = self.cleaned_data
if data.get('has_custom_name') and not data.get('name'):
raise forms.ValidationError("You must enter a custom name.")
return data.get('name')
With keyOrder set, has_custom_name will be validated (and therefore present in self.cleaned_data) before name is validated.
The Django docs claim that it's in order of the field definition.
But I've found that it doesn't always hold up to that promise.
Source: http://docs.djangoproject.com/en/dev/ref/forms/validation/
These methods are run in the order
given above, one field at a time. That
is, for each field in the form (in the
order they are declared in the form
definition), the Field.clean() method
(or its override) is run, then
clean_(). Finally, once
those two methods are run for every
field, the Form.clean() method, or its
override, is executed.
There's no promise that the fields are processed in any particular order. The official recommendation is that any validation that depends on more than one field should be done in the form's clean() method, rather than the field-specific clean_foo() methods.
The Form subclass’s clean() method. This method can perform any
validation that requires access to multiple fields from the form at
once. This is where you might put in things to check that if field A
is supplied, field B must contain a valid email address and the like.
The data that this method returns is the final cleaned_data attribute
for the form, so don’t forget to return the full list of cleaned data
if you override this method (by default, Form.clean() just returns
self.cleaned_data).
Copy-paste from https://docs.djangoproject.com/en/dev/ref/forms/validation/#using-validators
This means that if you want to check things like the value of the email and the parent_email are not the same you should do it inside that function. i.e:
from django import forms
from myapp.models import User
class UserForm(forms.ModelForm):
parent_email = forms.EmailField(required = True)
class Meta:
model = User
fields = ('email',)
def clean_email(self):
# Do whatever validation you want to apply to this field.
email = self.cleaned_data['email']
#... validate and raise a forms.ValidationError Exception if there is any error
return email
def clean_parent_email(self):
# Do the all the validations and operations that you want to apply to the
# the parent email. i.e: Check that the parent email has not been used
# by another user before.
parent_email = self.cleaned_data['parent_email']
if User.objects.filter(parent_email).count() > 0:
raise forms.ValidationError('Another user is already using this parent email')
return parent_email
def clean(self):
# Here I recommend to user self.cleaned_data.get(...) to get the values
# instead of self.cleaned_data[...] because if the clean_email, or
# clean_parent_email raise and Exception this value is not going to be
# inside the self.cleaned_data dictionary.
email = self.cleaned_data.get('email', '')
parent_email = self.cleaned_data.get('parent_email', '')
if email and parent_email and email == parent_email:
raise forms.ValidationError('Email and parent email can not be the same')
return self.cleaned_data