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)
Related
When a user registers on my app, an account verification link is sent to his email. When clicking on the link the first time, everything is fine and the account is verified, but when clicking on the same link again, the validation goes through, whereas it should raise an "authentication failed" error, since the "check_token" should return false, right?
Here's the verification serializer:
class VerifyAccountSerializer(serializers.Serializer):
uid = serializers.CharField(min_length=1, write_only=True)
token = serializers.CharField(min_length=1, write_only=True)
class Meta:
fields = ['uid', 'token']
def validate(self, attrs):
uid = attrs.get('uid')
token = attrs.get('token')
uidb64 = force_text(urlsafe_base64_decode(uid))
user = UserAccount.objects.get(pk=uidb64)
if user is None:
raise AuthenticationFailed('Invalid account. Please contant support')
if not PasswordResetTokenGenerator().check_token(user, token):
raise AuthenticationFailed('Account verify link is invalid. Please contant support.')
user.is_guest = False
user.save()
return user
And the view function:
#api_view(['POST'])
def verify_account(request):
if request.method == 'POST':
data = {}
serializer = VerifyAccountSerializer(data=request.data)
if serializer.is_valid():
user = serializer.validated_data
data['user'] = UserSerializer(user).data
data['token'] = AuthToken.objects.create(user)[1]
# delete previous token
tokens = AuthToken.objects.filter(user=user.id)
if len(tokens) > 1:
tokens[0].delete()
return Response(data, status=status.HTTP_200_OK)
data = serializer.errors
return Response(data, status=status.HTTP_400_BAD_REQUEST
It's kinda weird why it's not raising an error, because, in my other serializer for resetting the password via a link as well, I have the exact same thing going on, except there's one more extra field for the password, and if click on that link for the second time, I get a validation error.
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 am trying to make one form for both inserting and updating data. I have read these:
Model validation on update in django
django exclude self from queryset for validation
In my project, however, I am not using ModelForm.
forms.py:
This is the form the user sees when registering his/her username and first_name. It is also the form an existing user sees when trying to change his/her username and/or first_name.
from django import forms
from .models import User
class SettingsForm(forms.Form):
username = forms.CharField(max_length=16)
first_name = forms.CharField(max_length=32)
# ... and many more form fields
def clean_slug(self):
"""Make sure that the username entered by the user will be unique in the database"""
username = self.cleaned_data['username']
try:
product = User.objects.get(username=username)
except User.DoesNotExist:
# Good, there is no one using this username
pass
else:
# There is alreaady a user with this username
raise forms.ValidationError('This username has been used. Try another.')
return username
The form cleaning works as intended for inserting data. However, when updating data, it complains that the username has been used (naturally, since the username already exists in the database).
How do I update the data without raising ValidationError when using a Form (and without using ModelForm)?
(The reasons for not using ModelForm in this case are: we may stop using the the orm, SettingsForm may contain a mix of fields from different models, some fields may be repeated hundreds of times in the form that is displayed to the user, we also need custom fields not tied to any model, ... and other scenarios that make using ModelForm quite challenging (impossible?). The purpose of this question is to find out if there are ways of achieving the desired result without using ModelForm.)
You have three cases:
The user is new
The user exists and doesn't change his/her username
The user exists and changes his/her username
You need to check if the username already exists only in the first two cases.
If it's an existing user you should pass the User instance to the form so you can use it in the clean_slug function, let's say in self.user variable.
Then you could just add two lines in the clean_slug function and it should work as you wish:
def clean_slug(self):
"""Make sure that the username entered by the user will be unique in the database"""
username = self.cleaned_data['username']
# If the user exists and the username has not been changed,
# just return the username
if self.user and self.user.username == username:
return username
try:
product = User.objects.get(username=username)
except User.DoesNotExist:
# Good, there is no one using this username
pass
else:
# There is alreaady a user with this username
raise forms.ValidationError('This username has been used. Try another.')
return username
The ValidationError is obviously because you're instantiating the SettingsForm when the username already exists, as you've already stated.
So if you want to add a form that can do double-duty, I would add an __init__ to SettingsForm that takes an is_update and saves it as a member variable, like so...
def __init__(self, is_update=False, **kwargs):
self.is_update = is_update
return super(SettingsForm, self).__init__(**kwargs)
then modify your clean_slug() to be:
def clean_slug(self):
username = self.cleaned_data['username']
try:
product = User.objects.get(username=username)
except User.DoesNotExist:
# Good, there is no one using this username
pass
else:
if not self.is_update: # for 'create' usage
# There is already a user with this username
raise forms.ValidationError('This username has been used. Try another.')
else: # for 'update' usage
pass
return username
You actually want your form to do two different things depending if it is a create or an update.
So either have two forms with a different clean_slug method or pass in an argument telling the form if it is an update or a create (there is another answer from neomanic showing this way).
Personally I think the easiest way would be to subclass your form and change the clean slug method. The use the new form for updates and your original form for creates.
class UpdateSettingsForm(settingsForm):
def clean_slug(self):
username = self.cleaned_data['username']
return username
I'm confused how to implement methods in serializers and views in DRF:
I have an account model extending AbstractBaseUser. The viewset looks like this:
class AccountViewSet(viewsets.ModelViewSet):
lookup_field = 'username'
queryset = Account.objects.all()
serializer_class = AccountSerializer
def get_permissions(self):
if self.request.method in permissions.SAFE_METHODS:
return (permissions.AllowAny(), TokenHasReadWriteScope())
if self.request.method == 'POST':
return (permissions.AllowAny(), TokenHasReadWriteScope())
return (permissions.IsAuthenticated(), IsAccountOwner(), TokenHasReadWriteScope())
def create(self, request):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
Account.objects.create_user(**serializer.validated_data)
return Response(serializer.validated_data, status=status.HTTP_201_CREATED)
return Response({
'status': 'Bad request',
'message': 'Account could not be created with received data.'
}, status=status.HTTP_400_BAD_REQUEST)
The serializer like this:
class AccountSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, required=False)
confirm_password = serializers.CharField(write_only=True, required=False)
class Meta:
model = Account
fields = ('id', 'email', 'username', 'created_at', 'updated_at',
'first_name', 'last_name', 'tagline', 'password',
'confirm_password',)
read_only_fields = ('created_at', 'updated_at',)
def create(self, validated_data):
return Account.objects.create(**validated_data)
def update(self, instance, validated_data):
instance.username = validated_data.get('username', instance.username)
instance.save()
password = validated_data.get('password', None)
confirm_password = validated_data.get('confirm_password', None)
if password and confirm_password:
instance.set_password(password)
instance.save()
update_session_auth_hash(self.context.get('request'), instance)
return instance
def validate(self, data):
if data['password'] and data['confirm_password'] and data['password'] == data['confirm_password']:
try:
validate_password(data['password'], user=data['username']):
return data
except ValidationError:
raise serializers.ValidationError("Password is not valid.")
raise serializers.ValidationError("Passwords do not match.")
On the create method for the view, it checks if the serializer is valid then saves it and returns responses depending on the outcome. My first question is when is the serializer create() method called? To me it seems that the method is bypassed altogether by calling create_user (a model method) in the view's create() method. Does it get called at all? What is the point of having it?
Second, I'm having trouble returning a status code from the update method, the instance is saved in the serializer. Will the code inside serializer update() work if the validation fails?
Here is what I have so far:
def update(self, request, pk=None):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
<< what goes here??? >>
return Response(serializer.validated_data, status=status.HTTP_200_OK)
except serializers.ValidationError as e:
return Response({
'status': 'Bad request',
'message': str(e)
}, status=status.HTTP_400_BAD_REQUEST)
return Response({
'status': 'Bad request',
'message': 'Account could not be updated with received data.'
}, status=status.HTTP_400_BAD_REQUEST)
I desperately need some clarification. I'm unsure how to request flows through the view/serializer methods and I'm not sure how I can save the instance in the serializer and decide which response to return in the view at the same time.
EDIT:
I removed the create and update methods and fixed get_permissions for AccountViewSet and I added username validation to validate as you suggested. I also updated the serializer create and update methods, here are the new versions:
def create(self, validated_data):
instance = super(AccountSerializer, self).create(validated_data)
instance.set_password(validated_data['password'])
instance.save()
return instance
def update(self, instance, validated_data):
instance.username = validated_data.get('username', instance.username)
password = validated_data.get('password', None)
confirm_password = validated_data.get('confirm_password', None)
if password and confirm_password:
instance.set_password(password)
instance.save()
update_session_auth_hash(self.context.get('request'), instance)
else:
instance.save()
return instance
My only questions are is it necessary to call set_password after create? Does't create set the password for the new user? And is it okay to have no code in the view for create and update? Where does serializer.save() get called without the view code and when does the serializer validate run without a call to serializer.is_valid()?
In your create() method in AccountViewSet class, you are creating Account instance when serializer validation passes. Instead, you should be calling serializer.save().
If you have a look at save() method in BaseSerializer class you'll see that it calls either create() or update() method, depending on whether model instance is being created or updated. Since you are not calling serializer.save() in AccountViewSet.create() method, the AccountSerializer.create() method is not being called. Hope this answers your first question.
The answer to your second question too, is a missing serializer.save(). Replace << what goes here??? >> with serializer.save(). This (as I explained above), will call AccountSerializer.update() method.
AccountViewSet:
you do not need .create() and .update() methods, from your example - existing ones should be sufficient
get_permissions() - first "if" is opening your system too wide imho should be removed - you allow anyone to do POST - "aka" create new account, this is ok, but everything else (like GET or PUT) - should be allowed only for the account owner or (if there is a need!) registered users
AccountSerializer:
API will return HTTP400 on failed validation
make sure that id field is readonly, you do not want someone to overwrite existing users
existing create() method can be removed, but I think your's should looks like:
def create(self, validated_data):
instance = super(AccountSerializer, self).create(validated_data)
instance.set_password(validated_data['password'])
instance.save()
return instance
existing update() method... not sure what you wanted, but:
first line is permitting user to change it's username, without validation e.g. username is unique, even more, you do not check at all what is in the username field passed from the request, it may be even empty string or None - fallback on dictionary.get will be called only when key is missing in the dictionary,
if uniqueness of username will be validated on the db level (unique=True in the model's field definition) - you will get strange exception instead of nice error message for the API)
next is validation of the passwords (again) - that was just tested in validate method
after setting user password you save instance.. second time - maybe it is worth to optimize it and have only one save?
if you do not allowing to update all of the fields, maybe it is a good idea to pass update_fields to save() to limit which fields are updated?
validation - just add username validations ;)
Just to quickly understand DRF (very simplified) - Serializers are API's Forms, ViewSets - generic Views, renderers are templates (decide how data is displayed)
--edit--
few more items regarding viewsets, ModelViewSet consist of:
mixins.CreateModelMixin - calling validate & create -> serializer.save -> serializer.create
mixins.RetrieveModelMixin - "get" instance
mixins.UpdateModelMixin - calling validate & update/partial update -> serializer.save -> serializer.update
mixins.DestroyModelMixin - calling instance delete
mixins.ListModelMixin - "get" list of instances (browse)
I have a user registration form, and I want the initial password to be the same as the social security number. For some reason, I need to keep the actual password input, so I only hid it.
Now I'm struggling with setting the value of the password before it gets validated. I was under the impression that clean() calls the validation stuff, so naturally I wrote this:
def clean(self):
self.data['password1'] = self.data['password2'] = self.data['personal_number']
return super(SomeForm, self).clean()
This is however not working, because the field apparently gets validated before I can populate it. Help?
def clean_password1(self):
return self.cleaned_data['personal_number']
def clean_password2(self):
return self.cleaned_data['personal_number']