I'm trying to validate a form through DRF, but it would require the user to enter their password for confirmation. I can't seem to get it to work. Here is my current View and Serializer. Its for a 'change email' form, two fields required, the email and user password. It's for a seperate email model. The serializer:
class UpdateEmailAddressSerializer(serializers.ModelSerializer):
class Meta:
model = EmailAddress
fields = ('email',)
And the APIView:
class UpdateEmailAPI(APIView):
permission_classes = (IsAuthenticated,)
serializer_class = UpdateEmailAddressSerializer
def post(self, request, user, format=None):
user = User.objects.get(username=user)
serializer = UpdateEmailAddressSerializer(data=request.data, instance=user)
if serializer.is_valid():
## logic to check and send email
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
I'm not sure where to place the password or what to do with it. Its from the User model itself. When I attempted to add password to the fields in the UpdateEmail serializer it ended up updating the User password with plain text and making that user object unable to use that password.
I just want to check the password of the user for confirmation of this form. Is there an obvious way to do this?
EDIT
When I attempt to bring 'password' into the serializer, an error tells "Field name password is not valid for model EmailAddress." So when I attempt to bring it in e.g.
password = serializers.CharField(required=True)
or try:
## UserPasswordSerializer
class UserPasswordSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = (
'password',
)
## In UpdateEmailAddressSerializer
password = UserPasswordSerializer()
I get this error when submitting the form on DRF:
Got AttributeError when attempting to get a value for field
password on serializer UpdateEmailAddressSerializer. The
serializer field might be named incorrectly and not match any
attribute or key on the EmailAddress instance. Original exception
text was: 'EmailAddress' object has no attribute 'password'
So it seems to be telling me password isn't part of EmailAddress model which is correct. But I cant figure out how to simply check the password alongside the form post without making it part of EmailAddress.
I think you can try like this:
class UpdateEmailAddressSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
class Meta:
model = EmailAddress
fields = ('email', 'password',)
def create(self, validated_data):
validated_data.pop('password', None)
return super(UpdateEmailAddressSerializer, self).create(validated_data)
def update(self, instance, validated_data):
if instance.check_password(validated_data.get('password')):
instance.email = validated_data.get('email', instance.email)
# else throw validation error
return instance
Related
I am trying to auto generate username for users as they sign up, however, I do not know where to set the username. I do not want to user signals, so therefore I want to set the username for users when the user signs up.
In my UserCreationForm I have remove the username by:
users = None
and inside the SingUpForm(UserCreationForm) method I have:
class SignUpForm(UserCreationForm):
class Meta:
fields = (
"first_name",
"last_name",
"email",
)
However, I do not know how to set the username for the user, should I set the username for user inside the signup view?
user = form.save(commit=False)
user.username = generate_username(user.first_name, user.last_name, user.email)
user.save()
Or should I have the generate_username() method inside my custom creation form?
Or should I have the generate_username() method inside my custom creation form?
That is probably more appropriate, since the form normally should be responsible to create a valid Userobject, you thus an override the def save method:
class SignUpForm(UserCreationForm):
class Meta:
fields = (
'first_name',
'last_name',
'email',
)
def save(self, *args, **kwargs):
user = self.cleaned_data
self.instance.username = generate_username(
user['first_name'],
user['last_name'],
user['email']
)
return super().save(*args, **kwargs)
then in the view, it is simply:
user = form.save()
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.
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"?
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
...
How would I add the auth token to the userSeralizer?
This is my serializer:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'username')
And then in my views the url:
#api_view(['POST', 'DELETE'])
def create_user(request):
"""
API endpoint to register a new user
"""
model = User
serializer_class = UserSerializer
username, password = request.POST['username'], request.POST['password']
try:
user = User.objects.create_user(username, username, password)
except IntegrityError:
user = User.objects.get(username=username, email=username)
# the users token, we will send this to him now.
token = Token.objects.get(user=user)
if request.method == "POST":
serializer = UserSerializer(user)
return Response(data)
I think it would be nice to have the token in the serializer, or not?
From a security standpoint, auth tokens should not be passed around in the serializer. If your User view can be seen by anyone, then anyone could to impersonate any user without much trouble.
Tokens are meant to be returned only after successful login, not when an user is created. This is why most sites require Users to sign in just after the account was created.
But for the sake of the question, there are several ways to add items to serializers.
First, is a little hacky but doesn't require custom models
# Not adding context would raise a DeprecationWarning in the console
serializer = UserSerializer(user, context={'request': request})
data = serializer.data
data['token'] = token
return Response(data)
Last but not least, is a bit more elegant but requires a custom User class. However you could use it in your app models.
# in models.py inside your User model
def get_my_token(self):
return Token.objects.get(user=user)
my_token = property(get_my_token)
and then in the serializer class add the field with the token (remember to add it to the fields attribute in your meta class)
class UserSerializer(serializers.ModelSerializer):
token = serializers.Field(source='my_token')
class Meta:
model = User
fields = ('id', 'username', 'token')