Overriding Serializer's update() Function Fails - django

I have a view that calls patch()
class ValidateResetPasswordView(views.APIView):
def patch(self, request, token):
serializer = ValidateResetPasswordRequestSerializer(instance=request.user, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
.
.
.
etc
My serializer overrides update() to encrypt the password as
class ValidateResetPasswordRequestSerializer(serializers.Serializer):
password = serializers.CharField(max_length=128, required=True, allow_blank=False, allow_null=False,
write_only=True)
def update(self, instance, validated_data):
instance.set_password(validated_data.get("password"))
instance.save()
return instance
My serializer doesn't catch empty requests. For example, if a client sends out an empty json, my patch() gets processed successfully given no key was provided.
{
}
I expect to get an error that says password is required, or the like. To prevent this issue, I had to manually validate whether a key exists or not within update() function as follows.
class ValidateResetPasswordRequestSerializer(serializers.Serializer):
password = serializers.CharField(max_length=128, required=True, allow_blank=False, allow_null=False,
write_only=True)
def update(self, instance, validated_data):
if len(validated_data) == 0: # force validation
raise serializers.ValidationError(
'Password is required')
validated_data.get("password")
instance.set_password(validated_data.get("password"))
instance.save()
return instance
Am I violating any coding standards in here. Is there a better, correct, way to validate against empty requests?

You are doing it right with writing the Validator Serializer. There is no need for update(), you just need to validate incoming data in your view. So, your validator is going to look like this (though I suggest using min_length for password validation):
class ValidateResetPasswordRequestSerializer(serializers.Serializer):
password = serializers.CharField(max_length=128, required=True, allow_blank=False, allow_null=False,
write_only=True)
The way to integrate it with your view:
class ResetPasswordView(CreateAPIView):
serializer_class = ValidateResetPasswordRequestSerializer
def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid(raise_exception=True)
# update password here and return status code 200
You set your serializer to be ValidateResetPasswordRequestSerializer, you pass request data to it by saying self.serializer_class(data=request.data) and then you check if recieved data is valid with serializer.is_valid(raise_exception=True). If data is not valid Django will raise an exceptions since flag is set to True with relevant message to user (something like 'Password is required', 'Blank is not allowed' etc).

Related

django REST framework - AttributeError: 'ResetPasswordRequestSerializer' object has no attribute 'id'?

I am trying to set-up RequestPasswordResetAPI endpoint.
# Serializer
class ResetPasswordRequestSerializer(serializers.Serializer):
email = serializers.EmailField(min_length=2)
class Meta:
fields = ['email']
def validate(self, data):
print(data)
# Check if email exists
if data.get('email', ''):
try:
# Get the user
user = User.objects.get(email=data.get('email', ''))
print(f"User from validate {user}")
return user
except:
print('exception')
raise serializers.ValidationError("Email is not registered")
raise serializers.ValidationError("Email is not registered")
api.py
class ResetPasswordRequesAPI(generics.GenericAPIView):
serializer_class = ResetPasswordRequestSerializer
def post(self, request, *args, **kwargs):
print('make request')
serializer = self.get_serializer(data=request.data)
print('first line done')
serializer.is_valid(raise_exception=True)
# user = serializer.save()
user = serializer.validated_data
print(user)
user = ResetPasswordRequestSerializer(
user, context=self.get_serializer_context())
print(f"user is {user}")
uidb64 = urlsafe_base64_encode(user.id)
token = PasswordResetTokenGenerator().make_token(user)
print(f"second user is {user}")
print(token)
At the uidb64 = urlsafe_base64_encode(user.id) I get:
AttributeError: 'ResetPasswordRequestSerializer' object has no attribute 'id'
When I look at the output of various print(user) statements I have added all over:
user:
ResetPasswordRequestSerializer(<User: john>, context={'request': <rest_framework.request.Request: POST '/api/auth/reset_password/'>, 'format': None, 'view': <accounts.api.ResetPasswordRequesAPI object>})
Trying to understand why:
# In serializer
user = User.objects.get(email=data.get('email', ''))
Is only giving the User without id and other fields. When I try to generate token for reset I get more AttributeErrors.
The issue lies in that you have not yet saved your model user at the moment you validated the data.
The serializer method is to first validate the input data, then to save it. E.g.:
# first process request data into serializer
serializer = self.get_serializer(data=request.data)
# then validate data
serializer.is_valid(raise_exception=True)
# and save model, which creates an ID
user = serializer.save()
print(f'Model save, id is: {user.id}')
Check out this tutorial of RestFramework that shows it working together nicely, if serializer.is_valid(): serializer.save()

Django: DRF custom password change view and serializer not working

I have defined a serializer class and a view class to perform password change. As usual, user needs to enter old password, then the new password for two times to confirm. I am using AbstractBaseUser to implement a custom user. I defined the old_password_validator and new_password_validator with suggestions from this thread, though I don't know how to make it work.
I am using djangorestframework_simplejwt for all sorts of authentication.
My serializer class:
class ChangePasswordSerializer(serializers.ModelSerializer):
old_password = serializers.CharField(required=True, write_only=True)
new_password = serializers.CharField(required=True, write_only=True)
re_new_password = serializers.CharField(required=True, write_only=True)
def update(self, instance, validated_data):
instance.password = validated_data.get('password', instance.password)
if not validated_data['new_password']:
raise serializers.ValidationError({'new_password': 'not found'})
if not validated_data['old_password']:
raise serializers.ValidationError({'old_password': 'not found'})
if not instance.check_password(validated_data['old_password']):
raise serializers.ValidationError({'old_password': 'wrong password'})
if validated_data['new_password'] != validated_data['re_new_password']:
raise serializers.ValidationError({'passwords': 'passwords do not match'})
if validated_data['new_password'] == validated_data['re_new_password'] and instance.check_password(validated_data['old_password']):
instance.set_password(validated_data['new_password'])
instance.save()
return instance
class Meta:
model = User
fields = ['old_password', 'new_password','re_new_password']
I defined my view class like this:
class ChangePasswordView(generics.UpdateAPIView):
permission_classes = (permissions.IsAuthenticated,)
def update(self, request, *args, **kwargs):
serializer = ChangePasswordSerializer(data=request.data)
if serializer.is_valid(raise_exception=True):
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
When I entered the values, headers and ran in postman I got this error:
Got a TypeError when calling User.objects.create(). This may be because you have a writable field on the serializer class that is not a valid argument to User.objects.create(). You may need to make the field read-only, or override the ChangePasswordSerializer.create() method to handle this correctly.
I can't make anything out of this error statement.
You need to pass instance argument for ChangePasswordSerializer:
serializer = ChangePasswordSerializer(instance=self.request.user, data=request.data)
When you don't pass instance argument, serializer runs create() method when you call save() otherwise (when instance arg provided) update() is called.

Triggering a django rest framework update() function using HTTP Requests

I just started to learn django last week so please excuse my ignorance if I'm completely approaching this problem the wrong way.
So I've been following a thinkster tutorial on setting up a User model that allows the change of a password in the model. So far I have a url (/api/user) that leads to this view:
class UserRetrieveUpdateAPIView(RetrieveUpdateAPIView):
permission_classes = (IsAuthenticated,)
renderer_classes = (UserJSONRenderer,)
serializer_class = UserSerializer
def retrieve(self, request, *args, **kwargs):
#turns the object recieved into a JSON object
serializer = self.serializer_class(request.user)
return Response(serializer.data, status=status.HTTP_200_OK)
def update(self, request, *args, **kwargs):
serializer_data = request.data
serializer = self.serializer_class(
request.user, data=serializer_data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
I understand that this section :
serializer = self.serializer_class(
request.user, data=serializer_data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
will call upon a serializer class along the lines of:
class UserSerializer(serializers.ModelSerializer):
#This class handles serialization and deserialization of User objects
password = serializers.CharField(
max_length=128,
min_length=8,
write_only=True
)
class Meta:
model = User
fields = ('email', 'username', 'password', 'token',)
read_only_fields=('token',)
def update(self, instance, validated_data):
#performs an update on the user
password = validated_data.pop('password', None)
#have to take out password because setattr does not handle hashing etc
for (key, value) in validated_data.items():
#for the keys after taking out password set them to the updating User instance
setattr(instance, key, value)
if password is not None:
instance.set_password(password) #set_password is handled bydjango
instance.save() #set_password does not save instance
return instance
again I understand this section will essentially take request.data and "update" the model. However I'm stuck on how to test this feature using Postman.
Currently when I send a GET request to the URL using Postman I get this response:
GET Request result
The response is based off of my authenticate class that uses JWT authentication.
My question is, how do I trigger that update function using a Postman HTTP Request.
PATCH(partial_update) or PUT(update) http://127.0.0.1:8000/api/user/user_id/
you can see router table here

How do I prevent the creation of a user if validation on the nested serializer fails?

I am creating a REST api for user registration, and I have a nested serializer where I store additional information about a user.
The User serializer asks for first_name, last_name, email, and password.
The nested serializer asks for agreed_terms_of_service
email, password, and agreed_terms_of_service are required.
But if a user keys in their email and password and DOES NOT check the agreed_terms_of_service box, it returns and error, but still creates a user with the email and password.
Then when the user goes to 'remedy the situation', the email address is already in use.
If I update instead of create, I feel like I would run into a situation where people are overwriting other users...
I am wondering how people handle this with django rest serializers and what is the best practice?
VIEWS.PY
def serialize(self, request):
if request.method =='POST':
data = json.loads(request.body)
#first validation
if data['password'] != data['password2']:
raise serializers.ValidationError({'msgType':'error','message':'Passwords do not match.'})
#move to serializer
else:
serializer = userSerializer(data = data)
data['username'] = data['email']
if serializer.is_valid(raise_exception=True):
serializer.save()
response = {'msgType':'success', 'message':'Your account has been created successfully.'}
elif serializer.errors:
raise serializers.ValidationError({'msgType':'error', 'message': serializer.errors})
return Response(response)
SERIALIZERS.PY
class nestedSerializer(serializers.ModelSerializer):
class Meta:
model = Nested
fields = ('agreed_terms_of_service')
def validate(self, data):
return data
class userSerializer(serializers.ModelSerializer):
nested = nestedSerializer()
class Meta:
model = User
fields = ('pk','email', 'password', 'username','first_name','last_name','nested')
def validate(self, data):
email = data['email']
try:
User.objects.get(email = email)
except User.DoesNotExist:
return data
else:
raise serializers.ValidationError({'msgType':'error', 'message':'A user with this email address already exists.'})
return data
def create(self, validated_data):
nested_data = validated_data.pop('extend')
email = validated_data['email']
user = User.objects.create(**validated_data)
user.username = user.id
user.set_password(validated_data['password'])
user.save()
nested = Nested.objects.create(user=user, **nested_data)
return user
Models.py
class Nested(models.Model):
user = models.OneToOneField(User)
personalid = models.CharField(max_length=255)
agreed_terms_of_service = models.BooleanField()
city = models.CharField(max_length=255, blank=True, null=True)
Thank you for your help in advance. It is much appreciated.
First, I'd change your current validate() function to validate_email() (because all you're doing is validating that the email is not already in use). You should use validate() if you want access to multiple fields in your function. See the documentation here to read more about when you should use field-level validation and object-level validation: http://www.django-rest-framework.org/api-guide/serializers/#validation
Second, in your view, you do:
if data['password'] != data['password2']:
raise serializers.ValidationError({'msgType':'error','message':'Passwords do not match.'})
If you're verifying that "password" and "confirm password" field match, I'd do that check in the validate() function of your serializer (since you'll be accessing both the 'password' and the 'password2' field.
Third, in your create method, I'd use User.objects.create_user to create a user (create_user will handle the hashing of the password, etc. That way, you don't need to explicitly do user.set_password(validated_data['password'])). See the answer here for more information: How to create a user in Django?
Lastly, to address the main issue. Your "agreed_terms_of_service" is a Boolean field, which means it accepts both True and False. What I'd try is this:
class nestedSerializer(serializers.ModelSerializer):
class Meta:
model = Nested
fields = ('agreed_terms_of_service')
def validate_agreed_terms_of_service(self, data):
print(data) # to verify if data is even a boolean
if data == True or data == 'True':
return data
raise serializers.ValidationError({'msgType':'error', 'message':'Please accept the terms and conditions.'})
and in your create function for your userSerializer, add a print statement at the beginning to see if create is being executed before the "agreed_terms_of_service" validation.
def create(self, validated_data):
print("Creating the object before validating the nested field.")
# I'd be surprised if DRF executes a create function before
# even validating it's nested fields.
# rest of the create code goes here
When you add the statements above, what does it print for "data" and does it print "data" before "creating the object"?

How to get authenticated user on serializer class for validation

I'm working on a project using django-rest-framework. In my API view, an authenticated user can create other users. But, only five. Then if there are five users registered by one user, I want to send him in the response that hit the limit. Then, I need to get on my serializer the authenticated user but, I can't find a way to pass it from my ModelViewSet to my serializer.
This is my code:
View:
class ChildUserViewSet(viewsets.ModelViewSet):
serializer_class = ChildUserSerializer
queryset = User.objects.all()
authentication_classes = (
TokenAuthentication,
)
permission_classes = (
IsAuthenticated,
)
def perform_create(self, serializer):
account_group = self.request.user.userprofile.get_account_group
mobile_number = serializer.data.get('mobile_number')
password = serializer.data.get('password')
user = serializer.save()
user.set_password(password)
user.save()
# Generate user profile
UserProfile.objects.create(
user=user,
mobile_number=mobile_number,
user_type=CHILD,
related_account_group=account_group,
)
Serializer:
class ChildUserSerializer(serializers.ModelSerializer):
mobile_number = serializers.CharField()
class Meta:
model = User
fields = (
'first_name',
'last_name',
'email',
'password',
'mobile_number',
)
def validate(self, data):
"""
Check that the start is before the stop.
"""
# Get authenticated user for raise hit limit validation
def validate_email(self, value):
if User.objects.filter(email=value):
raise serializers.ValidationError("This field must be unique.")
return value
def create(self, validated_data):
username = generate_unique_username(
u'{0}{1}'.format(
validated_data['first_name'],
validated_data['last_name'],
)
)
user = User(
username=username,
first_name=validated_data['first_name'],
last_name=validated_data['last_name'],
email=validated_data['email'],
)
user.set_password(validated_data['password'])
user.save()
return user
Then, in the def validate(self, data) function of my serializer, I want to get the currently authenticated user.
How can I pass the request.user from my APIView to my serializer?
I found an even easier way of accomplishing this! It turns out that Rest Framework's GenericAPIView base class (from which all of Rest Framework's generic View classes descend) includes a function called get_serializer_context():
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
return {
'request': self.request,
'format': self.format_kwarg,
'view': self
}
As you can see, the returned context object contains the same request object that the View receives. This object then gets set when the serializer is initialized:
def get_serializer(self, *args, **kwargs):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
serializer_class = self.get_serializer_class()
kwargs['context'] = self.get_serializer_context()
return serializer_class(*args, **kwargs)
Thus to access the user who made the request, you just need to call self.context['request'].user from within your Serializer's validate_ function:
class TemplateSerializer(serializers.ModelSerializer):
def validate_parent(self, value):
print(self.context['request'].user)
return value
class Meta:
model = Template
And the best part is that you don't have to override anything in your ModelViewSet, they can stay as simple as you want them to:
class TemplateViewSet(viewsets.ModelViewSet):
serializer_class = TemplateSerializer
permission_classes = [IsAdmin]
In your views when you initialize serializer like
serializer = ChildUserSerializer(data=request.DATA,context={'request':request})
,send a context which contains request.Then in Serializers inside function call
request=self.context['request']
Then you can access request.user.
You can pass additional context to your serializer with serializer = ChildUserSerializer(data, context={'request': request}). You can then access the authenticated user via request.user within your serializer validation method.
In djangorestframework > 3.2.4 the rest_framework.generic.GenericAPIView class includes the http request by default in the serializer context.
So inside your serializer you can access it by: self.context['request'] and the user self.context['request'].user
So your ChildUserSerializer will look like:
class ChildUserSerializer(serializers.ModelSerializer):
mobile_number = serializers.CharField()
....
def validate(self, data):
"""
Check that the start is before the stop.
"""
# Get authenticated user for raise hit limit validation
user = self.context['request'].user
# do something with the user here
def validate_email(self, value):
if User.objects.filter(email=value):
raise serializers.ValidationError("This field must be unique.")
return value
...