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:
id
field is readonly, you do not want someone to overwrite existing usersexisting 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:
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,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)validate
methodupdate_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:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With