What are the differences between using self.add_error() and raising a ValidationError()? - django

You can throw a validation error in 2 ways. The first is with self.add_error() and the second with raise ValidationError().
I've read that when you use self.add_error('field1','description here') then field1 is also automatically removed from the cleaned_data list and i assume a ValidationError object is also added to the self.errors list, is this correct?
But what happens when you don't choose to use self.add_error and opt for using raise ValidationError instead? Is this object also automatically added to the errors list behind the scenes? And how would you display this error message as caption under the correct invalid field?
Thank you

If you raise an error, the control flow of that method, and callers of that method stops, until there is a method that has span a try-except over that, and catches the exception accordingly.
But sometimes a field might contain multiple errors. For example if you have a password, you might want to add errors because it is too short, does not contain a digit, a lowercase, and/or upppercase.
Then you thus can implement this with:
def clean_password(self):
pwd = self.cleaned_data['password']
if len(pwd) < 10:
self.add_error('password', 'The password is too short.')
if not any(c.isupper() for c in pwd):
self.add_error('password', 'The password should contain an uppercase character.')
if not any(c.islower() for c in pwd):
self.add_error('password', 'The password should contain an lowercase character.')
if not any(c.isdigit() for c in pwd):
self.add_error('password', 'The password should contain an digit.')
return pwd
If you would raise a ValidationError for one of these, it can not add mutliple problems that a password might have.
You can however pass a list of errors to a ValidationError data constructor, hence you can use the two interchangeable.

Related

Check that ValidationError is actually the one I expect in a test

I have a test which creates a Django object "forgetting" to set one field. It should fail, so I check:
p = Person.objects.create(name='test')
self.assertRaises(ValidationError, p.full_clean)
The forgotten field is birth_date, so if I intercepted the exception and printed it, the error would be:
{'birth_date': ['This field cannot be blank.']}`
Now I added another field to Person, which also shouldn't be left blank. The error now would be:
{'birth_date': ['This field cannot be blank.'], 'category': ['This field cannot be blank.']}
With the above code, I silently suppress both errors. I need to test that the code raises a specific validation error. Is there a good way to do it? For now I've this workaround:
try:
p.full_clean()
except ValidationError as e:
self.assertIn('birth_date', dict(e))
self.assertEqual(len(dict(e).keys()), 1, msg="Expected one error, found more")
But if there could have been two different errors with 'birth_date', this would catch either.
Is there a better way?

Django 1.9 "Common Password Validator" - Strange Behaviour

I'm attempting to replace the built in common-passwords.txt.gz file, which supposedly contains the top 1,000 common passwords, with my own identical version which contains the top 10,000 common passwords for my country, but I've encountered some rather strange behaviour.
Firstly I directly substituted Django's common-passwords.txt.gz file (4KB) with my own containing my .txt file with the same utf-8 encoding as Django (which comes in at 34KB), then restarted the test server. When changing a users password to "password" it does not raise the expected error as it does with Django's common password file.
The first line of both the built in password list and my new one begins 123456password12345678qwerty123456789... so it clearly should do.
When I append a few extra passwords to their common-passwords file it appears to work as it should and raise an error if I try to use them as passwords, so I don't think that it's cached somewhere or anything like that.
Is there some kind of built in file size limit for the common password list or for the gzip.open(password_list_path).read().decode('utf-8').splitlines() function?
Secondly, trying to figure out the above led me to a strange bug. Using Django's built in common-passwords.txt.gz (of which the first line starts 123456password12345678qwerty123456789...) successfully raises a validation error for "password" and "password1", but not for "password12" or "password123"!
As I read it, the Django validation code basically checks if the submitted password is in each line from the common passwords file, and I cannot find any code that exempts passwords above a certain length from the validation. Am I missing something or is this a bug?
The "common password validation" function in Django 1.9 is found in \venv\Lib\site-packages\django\contrib\auth\password_validation.py, the relevant class is below:
class CommonPasswordValidator(object):
"""
Validate whether the password is a common password.
The password is rejected if it occurs in a provided list, which may be gzipped.
The list Django ships with contains 1000 common passwords, created by Mark Burnett:
https://xato.net/passwords/more-top-worst-passwords/
"""
DEFAULT_PASSWORD_LIST_PATH = os.path.join(
os.path.dirname(os.path.realpath(upath(__file__))), 'common-passwords.txt.gz'
)
def __init__(self, password_list_path=DEFAULT_PASSWORD_LIST_PATH):
try:
common_passwords_lines = gzip.open(password_list_path).read().decode('utf-8').splitlines()
except IOError:
with open(password_list_path) as f:
common_passwords_lines = f.readlines()
self.passwords = {p.strip() for p in common_passwords_lines}
def validate(self, password, user=None):
if password.lower().strip() in self.passwords:
raise ValidationError(
_("This password is too common (it would be trivial to crack!)"),
code='password_too_common',
)
def get_help_text(self):
return _("Your password can't be a commonly used password.")
Finally got to the bottom of this!
There is some kind of invisible unrendered character in-between the passwords contained in Django's built in common passwords validation file, this explains both issues I encountered.
I changed my top 10k common passwords file to have the usual newline characters between them instead and now it all works great! Even though there are now 10 times as many passwords for it to compare against it still runs pretty much instantaneously!
I've uploaded my 10,000 most common passwords file to github for any future people who encounter this issue or who just want to improve Django's built-in common password validation: https://github.com/timboss/Django-Common-Password-Validation/

How to handle "matching query does not exist" when getting an object

When I want to select objects with a get() function like
personalProfile = World.objects.get(ID=personID)
If get function doesn't return find a value, a "matching query does not exist." error occurs.
If I don't need this error, I'll use try and except function
try:
personalProfile = World.objects.get(ID=personID)
except:
pass
But I think this is not the best way since I use
except:
pass
Please recommend some idea or code sample to fight with this issue
That depends on what you want to do if it doesn't exist..
Theres get_object_or_404:
Calls get() on a given model manager, but it raises Http404 instead of the model’s DoesNotExist exception.
get_object_or_404(World, ID=personID)
Which is very close to the try except code you currently do.
Otherwise theres get_or_create:
personalProfile, created = World.objects.get_or_create(ID=personID)
Although, If you choose to continue with your current approach, at least make sure the except is localised to the correct error and then do something with that as necessary
try:
personalProfile = World.objects.get(ID=personID)
except MyModel.DoesNotExist:
raise Http404("No MyModel matches the given query.")
The above try/except handle is similar to what is found in the docs for get_object_or_404...
A get_or_none() function has been proposed, multiple times now. The rejection notice is feature creep, which you might or might not agree with. The functionality is present --with slightly different semantics-- in the first() queryset method.
But first things first:
The manager throws World.DoesNotExist, a specialized subclass of ObjectDoesNotExist when a World object was not found:
try:
personalProfile = World.objects.get(ID=personID)
except World.DoesNotExist:
pass
There's also get_object_or_404() which raises a Http404 exception when the object was not found.
You can also roll your own get_or_none(). A possible implementation could be:
def get_or_none(queryset, *args, **kwargs):
try:
return queryset.get(*args, **kwargs)
except ObjectDoesNotExist:
return None
Note that this still raises MultipleObjectsReturned when more than one matching object is found. If you always want the first object regardless of any others, you can simplify using first(), which returns None when the queryset is empty:
def get_or_none(queryset, *args, **kwargs):
return queryset.filter(*args, **kwargs).first()
Note however, for this to work reliably, you need a proper order for objects, because in the presence of multiple objects first() might be non-deterministic (it probably returns the first object from the database index used to filter the query and neither indexes not the underlying tables need be sorted or even have a repeatable order).
Use both, however, only when the use of the object to retrieve is strictly optional for the further program flow. When failure to retrieve an object is an error, use get_object_or_404(). When an object should be created when it does not exist, use get_or_create(). In those cases, both are better suited to simplify program flow.
As alasdair mentioned you could use the built in first() method.
It returns the object if it exists or None if it's not
personalProfile = World.objects.filter(ID=personID).first()

Django ValidationError - how to use this properly?

Currently there is code that is doing (from with the form):
# the exception that gets raised if the form is not valid
raise forms.ValidationError("there was an error");
# here is where form.is_valid is called
form.is_valid() == False:
response['msg']=str(form.errors)
response['err']='row not updated.'
json = simplejson.dumps( response ) #this json will get returned from the view.
The problem with this, is that it is sending err message to the client as:
__all__"There was an error."
I want to remove the "all" garbage from the error template that is returned. How can I go about doing this? it seems to get added deep in django form code.
It's because the error is not associated with any field in particular, but it's so called non-field error.
If you're only interested in non-field errors, just simply pass this to the response:
response['msg']=str(form.errors['__all__'])
errors is an instance of a subclass of dict with some special rendering code. Most of the keys are the fields of the form, but as the docs describe, raising ValidationError in clean produces an error message that isn't associated with any particular field:
Note that any errors raised by your Form.clean() override will not be associated with any field in particular. They go into a special “field” (called __all__), which you can access via the non_field_errors() method if you need to. If you want to attach errors to a specific field in the form, you will need to access the _errors attribute on the form, which is described later.
https://docs.djangoproject.com/en/dev/ref/forms/validation/
So you can either generate your string representation of the errors differently (probably starting with form.errors.values() or form.errors.itervalues(), and maybe using the as_text method of the default ErrorList class) or associate your error with a particular field of the form as described in the docs:
When you really do need to attach the error to a particular field, you should store (or amend) a key in the Form._errors attribute. This attribute is an instance of a django.forms.utils.ErrorDict class. Essentially, though, it’s just a dictionary. There is a key in the dictionary for each field in the form that has an error. Each value in the dictionary is a django.forms.utils.ErrorList instance, which is a list that knows how to display itself in different ways. So you can treat _errors as a dictionary mapping field names to lists.
If you want to add a new error to a particular field, you should check whether the key already exists in self._errors or not. If not, create a new entry for the given key, holding an empty ErrorList instance. In either case, you can then append your error message to the list for the field name in question and it will be displayed when the form is displayed.
https://docs.djangoproject.com/en/dev/ref/forms/validation/#form-subclasses-and-modifying-field-errors

django try exception statement causing IntegrityError:

We use paypal on our system to check whether a user has paid before and already has an account.
This morning I received a traceback that basically gave me an integrity error.
IntegrityError: (1062, "Duplicate entry 'user_1234_before' for key 2")
My statemtent looks as follows.
try:
user = User.objects.get(email=ipn_obj.payer_email)
except:
user_slug = ("%s_%s_before") % (ipn_obj.first_name, ipn_obj.last_name)
username = slugify(user_slug)
user = User.objects.create_user(username, ipn_obj.payer_email, 'testpassword')
user.first_name = ipn_obj.first_name
user.last_name = ipn_obj.last_name
user.save()
Thanks in advance.
Never, ever use a blank except statement. What's happening here is an excellent demonstration of why.
You've presumably used that try/except block to catch the User.DoesNotExist exception. However, your code is actually raising a completely different exception. Because you're swallowing it, it's impossible to know which one, but potentially ipn_obj isn't what you think it is and doesn't have a payer_email error, so you're getting AttributeError. Or, possibly, you're getting the User.MultipleObjectReturned exception.
Change your except to except User.DoesNotExist, and then debug your actual problem.
I think you have two user with different payer_mail but same first_name and last_name: username (made up of first and last name) is the same for the two users. Probably username is a unique key on User and gives you the error.
Apart from catching the right exception (User.DoesNotExist), if you want to retain your code, I think you should 'uniquify' the username using the email field (which I suppose is unique):
user_slug = ("%s_%s_before") % (ipn_obj.first_name, ipn_obj.last_name)
with:
user_slug = ("%s_%s_%s_before") % (ipn_obj.first_name, ipn_obj.last_name, ipn_obj.payer_email)
or:
user_slug = ("%s_before") % (ipn_obj.payer_email)
The exception is actually being raised by the creation (ie INSERT statement) of a user with an already took username.
You shall check for its existence before doing this insertion .