API to retrieve and update users

Example of an API that uses GET and PATCH / PUT

When using PUT, all the fields should be sent to the API for update. When using PATCH, not all fields are required for update. The fields not in the payload are not edited

What should this API do?

1) Should allow only authenticated user to retrive specific fields (set permission and authentication classes in the view and define get method. Specify the fields to be returned in UserSerializer)

2) Should not allow POST call (extend from RetrieveAndUpdateView)

3) Allow updates from authenticated users (define update method in UserSerializer)

rom django.test import TestCase
from django.contrib.auth import get_user_model
from django.urls import reverse
from rest_framework.test import APIClient
from rest_framework import status

ME_URL = reverse("user:me")


def create_user(**kwargs):
    return get_user_model().objects.create_user(**kwargs)


class PrivateUsersApiTests(TestCase):
    """Test API requests that require authentication"""

    def setUp(self):
        self.user = create_user(
            email='test@test.com',
            password='testpass',
            name='name'
        )

        self.client = APIClient()

    # test that authentication is required for users
    def test_retrieve_user_unautherized(self):
        """ Test that authentication is required for users"""
        res = self.client.get(ME_URL)
        self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED)

    # Test retrieving profile for logged in user
    # Also test if the response contain the expected data
    def test_retrieve_profile_successful(self):
        # log in client
        self.client.force_authenticate(user=self.user)
        res = self.client.get(ME_URL)

        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertEqual(res.data, {
            'name': self.user.name,
            'email': self.user.email
        })

    # Test if post if not allowed in to this api
    def test_post_me_not_allowed(self):
        self.client.force_authenticate(user=self.user)
        res = self.client.post(ME_URL, {})

        self.assertEqual(res.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)

    # test update user profile
    def test_update_user_profile(self):
        self.client.force_authenticate(user=self.user)
        payload = {'name': 'new name', 'password': 'new pass'}
        # with patch you can just send the fields to be updated.
        # other fields wont be affected.
        # with PUT you have to send the entire record
        res = self.client.patch(ME_URL, payload)
        self.user.refresh_from_db()

        self.assertEqual(self.user.name, payload['name'])
        self.assertTrue(self.user.check_password(payload['password']))
        self.assertEqual(res.status_code, status.HTTP_200_OK)

Implementation of API

Modify UserSerializer class to have update method

# This serializer is for for an API that will add data to a model
class UserSerializer(serializers.ModelSerializer):
    """serializer for user object"""

    class Meta:
        model = get_user_model()
        fields = ('email', 'password', 'name')

        # the password should be write only.
        # it should not be serialized when get is called
        # we specify extra kwargs for each field
        # list of accepted args for can be found under core argument section of
        # https://www.django-rest-framework.org/api-guide/fields/
        # for password field, args under serializer.CharField are also valid
        extra_kwargs = {'password': {'write_only': True, 'min_length': 5}}

    # create is called when we use the CreateAPI view
    # which takes a POST request to create a user
    def create(self, validated_data):
        return get_user_model().objects.create_user(**validated_data)

    # update a user, setting the password correctly and return it
    def update(self, model_instance, validated_data):
        # we have to upate password separately from other data
        # so pop the password if it is available or return none as default
        password = validated_data.pop('password', None)

        # update all other fields in the model
        user = super().update(model_instance, validated_data)
        if password:
            user.set_password(password)
            user.save()

        return user

Create a view that extends RetrieveAndUpdateView and define get method

# view for API retrieving and updating user info
class ManageUserView(generics.RetrieveUpdateAPIView):
    serializer_class = UserSerializer
    authentication_classes = (authentication.TokenAuthentication,)
    permission_classes = (permissions.IsAuthenticated,)

    # retrieve and return authenticated user
    # this method is also required for update (patch)
    def get_object(self):
        # authentication class assigns user to request
        return self.request.user