So I created a custom form field to validate for duplicate usernames. I'm using Django + Mongoengine as my database. I have that plugged and working with the django authentication system so I'm assuming it can be accessed from forms.py? Maybe that assumption is incorrect. So I have the field
class UsernameField(CharField):
def to_python(self, value):
if not value:
return ""
return value
def validate(self, value):
super(CharField, self).validate(value)
try:
# If the user object exists it won't throw an Error/Exception
user=User.objects.get(username=value)
raise ValidationError("Username already exists")
except:
pass
But when I actually use it in my form, it always seems to validate correctly even though I've called checked if form.is_valid() is True
You're raising exceptions in the try block but then snuffing them out in the except block with pass. Try this, it will check for the existing user and only fails if it exists.
try:
# If the user object doesn't exist, it validates
user=User.objects.get(username=value)
except django.core.exceptions.DoesNotExist:
pass
else:
#user does exist, barf.
raise ValidationError("Username already exists")
Bah, it was a dumb mistake on my part. For some reason I forgot that the Error I was trying to raise would be caught by the try and I would get sent to the except route. Changing it to this works
class UsernameField(CharField):
def to_python(self, value):
if not value:
return ""
return value
def validate(self, value):
super(CharField, self).validate(value)
usernameDuplicate = False
try:
# If the user object exists it won't throw an Error/Exception
user=User.objects.get(username=value)
usernameDuplicate = True
except:
pass
if usernameDuplicate==True:
raise ValidationError("Username already exists")
Related
I am writing a login page using wtforms .The fields are username and password.Below are the two validations defined for two fields.Is there a way to stop the execution of validation function on password field if validation method on username raises exception
def validate_username(self, field):
# Check if not None for that username!
print('Inside check_username')
if User.query.filter_by(username=field.data).first() is None:
raise ValidationError(f'Sorry username : {field.data} is not registered!')
def validate_password(self, field):
# Below is not the actual code.This method will contain code to check of valid password.
raise ValidationError(f'Sorry username : {field.data} is not registered!')
What is happening right now is both of the above methods are been called which is expected but I don't want to check for password if the username doesn't exist
You cannot stop wtforms for trying to validate each field.
I suggest you simply override validate:
def validate(self):
if not super().validate():
return False
if User.query.filter_by(username=self.username.data).first() is None:
self.errors["username"] = f'Sorry username : {self.username.data} is not registered!'
return False
if not check_that_password():
self.errors["password"] = f"Bad password for user {self.username.data}"
return False
return True
I'm trying to write a serializer (in Django REST Framework) to update a user's account details. Here is the update() method:
def update(self, instance, validated_data):
...
if all([item in self.validated_data for item in ["password", "confirm_password", "old_password"]]):
user = authenticate(username=self.context["request"].user.username, password=self.validated_data["old_password"])
if user is not None:
if self.validated_data["password"] == self.validated_data["confirm_password"]:
validate_password(self.validated_data["password"])
user.set_password(self.validated_data["password"])
user.save()
else:
raise serializers.ValidationError({"confirm_password": "Passwords do not match"})
else:
raise serializers.ValidationError({"old_password": "Password incorrect"})
self.validated_data.pop("password")
return super(UserInfoSerializer, self).update(instance, validated_data)
When I perform a PATCH request to the view with "password", "confirm_password" and "old_password" as fields, it appears to have worked. Then when I try to log into the account again, it fails (using both old and new passwords). When I check the admin settings and view the user I am trying to edit, I get the following:
Invalid password format or unknown hashing algorithm.
Raw passwords are not stored, so there is no way to see this user's
password, but you can change the password using this form.
I believe User.set_password() is supposed to handle hashing/etc. automatically, so why do I get this error?
You deleted password from self.validated_data but not from validated_data dict which passed to superclass's update method. Try this:
validated_data.pop("password") # remove self, just leave validated_data
return super(UserInfoSerializer, self).update(instance, validated_data)
In case anyone is interested in using my code, here is the final working code:
def update(self, instance, validated_data):
...
if all([item in validated_data for item in ["password", "confirm_password", "old_password"]]):
user = authenticate(username=instance.username, password=validated_data["old_password"])
if user is not None and user == instance:
if validated_data["password"] == validated_data["confirm_password"]:
validate_password(validated_data["password"])
instance.set_password(validated_data["password"])
instance.save() # change the password on the current instance object, otherwise changes will be overwritten
login(self.context["request"], instance) # without this line, the user is auto-logged out upon changing their password
else:
raise serializers.ValidationError({"confirm_password": "Passwords do not match"})
else:
raise serializers.ValidationError({"old_password": "Password incorrect"})
if "password" in validated_data:
validated_data.pop("password")
return super(UserInfoSerializer, self).update(instance, validated_data)
I see that forms.ChoiceField is using this code to validate the value:
def validate(self, value):
"""
Validates that the input is in self.choices.
"""
super(ChoiceField, self).validate(value)
if value and not self.valid_value(value):
raise ValidationError(
self.error_messages['invalid_choice'],
code='invalid_choice',
params={'value': value},
)
def valid_value(self, value):
"Check to see if the provided value is a valid choice"
text_value = force_text(value)
for k, v in self.choices:
if isinstance(v, (list, tuple)):
# This is an optgroup, so look inside the group for options
for k2, v2 in v:
if value == k2 or text_value == force_text(k2):
return True
else:
if value == k or text_value == force_text(k):
return True
return False
and forms.models.ModelChoiceField this code:
def validate(self, value):
return Field.validate(self, value)
Q1. Why Django uses validation to check if the selected value (from dropdown) is indeed in the choice list for forms.ChoiceField?
Q2. When Django uses the validation from Q1, to check if the value is indeed in the choice list, why does not also check if the selected value is in the model records for forms.models.ModelChoiceField?
The validation process starts from form.full_clean() where you have form._clean_fields() and form._clean_form executed in this order.
Now if you take a closer look at what form._clean_fields() do, you will probably notice that it only calls field.clean(value, initial) and collects the results into a cleaned_data dict. So the interesting part is at field.clean, lets see what happens there:
def clean(self, value):
"""
Validate the given value and return its "cleaned" value as an
appropriate Python object. Raise ValidationError for any errors.
"""
value = self.to_python(value)
self.validate(value)
self.run_validators(value)
return value
First, we have a to_python call, followed by validate and finishing with run_validators.
So in terms of ModelChoiceField when you reach the .validate method, your choice is already a Model instance, thats why, this kind of validation (from Q2) is happening inside the to_python method.
def to_python(self, value):
if value in self.empty_values:
return None
try:
key = self.to_field_name or 'pk'
value = self.queryset.get(**{key: value})
except (ValueError, TypeError, self.queryset.model.DoesNotExist):
raise ValidationError(self.error_messages['invalid_choice'], code='invalid_choice')
return value
one thing i can say is for forms.ChoiceField the input are coming from the user perspective means a user can use inspect element and enter a choice which doesnt appear from the backend .
but for models one the choices are directly coming from the backend or the database
class UserField(forms.EmailField):
def clean(self, value):
super(UserField, self).clean(value)
try:
User.objects.get(username=value)
raise forms.ValidationError("There is an existing account associated with this email.")
except User.DoesNotExist:
return value
the try except method is straightforward. However, I am having a hard time figuring out the
super(UserField, self).clean(value)
super(UserField, self).clean(value) is calling the ancestor method : forms.EmailField.clean(value) to check that the email is well formed.
I am trying to perform a validation such that you cannot delete a user if he's an admin. I'd therefore like to check and raise an error if there's a user who's an admin and has been marked for deletion.
This is my inline ModelForm
class UserGroupsForm(forms.ModelForm):
class Meta:
model = UserGroups
def clean(self):
delete_checked = self.fields['DELETE'].widget.value_from_datadict(
self.data, self.files, self.add_prefix('DELETE'))
if bool(delete_checked):
#if user is admin of group x
raise forms.ValidationError('You cannot delete a user that is the group administrator')
return self.cleaned_data
The if bool(delete_checked): condition returns true and stuff inside the if block gets executed but for some reason this validation error is never raised. Could someone please explain to me why?
Better yet if there's another better way to do this please let me know
The solution I found was to clean in the InlineFormSet instead of the ModelForm
class UserGroupsInlineFormset(forms.models.BaseInlineFormSet):
def clean(self):
delete_checked = False
for form in self.forms:
try:
if form.cleaned_data:
if form.cleaned_data['DELETE']:
delete_checked = True
except AttributeError:
pass
if delete_checked:
raise forms.ValidationError(u'You cannot delete a user that is the group administrator')
Although #domino's answer may work for now, the "kinda" recommended approach is to use formset's self._should_delete_form(form) function together with self.can_delete.
There's also the issue of calling super().clean() to perform standard builtin validation. So the final code may look like:
class UserGroupsInlineFormset(forms.models.BaseInlineFormSet):
def clean(self):
super().clean()
if any(self.errors):
return # Don't bother validating the formset unless each form is valid on its own
for form in self.forms:
if self.can_delete and self._should_delete_form(form):
if <...form.instance.is_admin...>:
raise ValidationError('...')
Adding to domino's Answer:
In some other scenarios, Sometimes user wants to delete and add object in the same time, so in this case delete should be fine!
Optimized version of code:
class RequiredImageInlineFormset(forms.models.BaseInlineFormSet):
""" Makes inline fields required """
def clean(self):
# get forms that actually have valid data
count = 0
delete_checked = 0
for form in self.forms:
try:
if form.cleaned_data:
count += 1
if form.cleaned_data['DELETE']:
delete_checked += 1
if not form.cleaned_data['DELETE']:
delete_checked -= 1
except AttributeError:
# annoyingly, if a subform is invalid Django explicity raises
# an AttributeError for cleaned_data
pass
# Case no images uploaded
if count < 1:
raise forms.ValidationError(
'At least one image is required.')
# Case one image added and another deleted
if delete_checked > 0 and ProductImage.objects.filter(product=self.instance).count() == 1:
raise forms.ValidationError(
"At least one image is required.")