Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django Rest Framework Serializers and Views

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()?

like image 378
ss7 Avatar asked May 31 '16 06:05

ss7


2 Answers

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.

like image 188
Amrullah Zunzunia Avatar answered Sep 19 '22 03:09

Amrullah Zunzunia


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)
like image 36
Jerzyk Avatar answered Sep 22 '22 03:09

Jerzyk