Home > Software design >  How do I pass 'user_id' to CustomerSerializer?
How do I pass 'user_id' to CustomerSerializer?

Time:07-30

I don't know how to pass user_id from requests.user.id and use it in the CustomerSerializer to save the object to the database. The error stems from the fact that user_id exists in the customer table in the database but it does not show up as a field to be passed in the rest_framework API frontend (only phone and profile_image do).

Here is the Customer model:

class Customer(models.Model):
    phone = models.CharField(max_length=14)
    profile_image = models.ImageField(blank=True, null=True)
    user = models.OneToOneField(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

Here is the ViewSet:

class CustomerViewSet(ModelViewSet):
    queryset = Customer.objects.all()
    permission_classes = [permissions.IsAdminUser]
    serializer_class = CustomerSerializer

    # Only admin users can make requests other than 'GET'
    def get_permissions(self):
        if self.request.method == 'GET':
            return [permissions.AllowAny()]
        return [permissions.IsAdminUser()]

    @action(detail=False, methods=['GET', 'PUT'])
    def me(self, request):
        customer, created = Customer.objects.get_or_create(user_id=request.user.id)
        if request.method == 'GET':
            serializer = CustomerSerializer(customer)
            return Response(serializer.data)
        elif request.method == 'PUT':
            serializer = CustomerSerializer(customer, data=request.data)
            serializer.is_valid(raise_exception=True)
            serializer.save()
            return Response(serializer.data)

... and here is the Serializer:

class CustomerSerializer(serializers.ModelSerializer):
    class Meta:
        model = Customer
        fields = ['id', 'user_id', 'profile_image', 'phone']
```python


... and when I try to save a new customer by sending a POST request to the endpoint with the following data:

```json
{
    "profile_image": "Images/image.png",
    "phone": "009293930"
}

I get the following error:

IntegrityError at /api/customers/
(1048, "Column 'user_id' cannot be null")
Request Method: POST
Request URL:    http://127.0.0.1:8000/api/customers/
Django Version: 4.0.6
Exception Type: IntegrityError
Exception Value:    
(1048, "Column 'user_id' cannot be null")
Exception Location: /home/caleb/.local/share/virtualenvs/Cribr-svgsjjVF/lib/python3.8/site-packages/pymysql/err.py, line 143, in raise_mysql_exception
Python Executable:  /home/caleb/.local/share/virtualenvs/Cribr-svgsjjVF/bin/python
Python Version: 3.8.10
Python Path:    
['/home/caleb/Desktop/Cribr',
 '/home/caleb/Desktop/Cribr',
 '/snap/pycharm-professional/290/plugins/python/helpers/pycharm_display',
 '/usr/lib/python38.zip',
 '/usr/lib/python3.8',
 '/usr/lib/python3.8/lib-dynload',
 '/home/caleb/.local/share/virtualenvs/Cribr-svgsjjVF/lib/python3.8/site-packages',
 '/snap/pycharm-professional/290/plugins/python/helpers/pycharm_matplotlib_backend']
Server time:    Thu, 28 Jul 2022 23:38:53  0000

I figured the issue here is that the serializer class is not getting the user_id value from the POST request. I tried passing request.user.id to the serializer from the viewset through a context object (i.e., context={'user_id': request.user.id}) but I couldn't figure out how to then add it to the validated data which the serializer passes to the save method.

Any help on this issue will be much appreciated. Thanks in advance.

CodePudding user response:

The benefit of using DRF and viewsets is that most of the work has already been done for you. In instances such as this, you usually just need to tweak a few things to get it working the way you want. I've re-written your solution for you below:

class CustomerViewSet(ModelViewSet):
    queryset = Customer.objects.all()
    permission_classes = [permissions.IsAdminUser]
    serializer_class = CustomerSerializer

    # Only admin users can make requests other than 'GET'
    def get_permissions(self):
        if self.request.method == 'GET':
            return [permissions.AllowAny()]
        return [permissions.IsAdminUser()]

    def get_object(self):
        customer, created = Customer.objects.get_or_create(user_id=self.request.user.id)
        return customer

    @action(detail=False, methods=['GET'])
    def me(self, request):
        return self.retrieve(request)

From the DRF docs:

The ModelViewSet class inherits from GenericAPIView and includes implementations for various actions, by mixing in the behavior of the various mixin classes.

The actions provided by the ModelViewSet class are .list(), .retrieve(), .create(), .update(), .partial_update(), and .destroy().

The ModelViewSet will also set up a urlconf for you, which, excluding list will expect an object pk (primary key) to be provided in the url to allow the view to know what resource in the database you are trying to access. In your case, you want to determine that resource based on the authentication credentials provided in the request. To do this, we can override get_object to get or create the customer based on the authenticated user's id.

The next change we make is to define our action for the GET method. We want to be able to retrieve a resource, without specifying the pk in the url conf, hence detail=False. We can then simply call the builtin retrieve function from this action, which in turn will use get_object to get and return the customer object.

Thirdly, your PUT request will be directed to update, which is inherited from ModelViewSet, so you don't need to do anything here as you've already overwritten get_object.

def update(self, request, *args, **kwargs):
    partial = kwargs.pop('partial', False)
    instance = self.get_object()
    serializer = self.get_serializer(instance, data=request.data, partial=partial)
    serializer.is_valid(raise_exception=True)
    self.perform_update(serializer)

    if getattr(instance, '_prefetched_objects_cache', None):
        # If 'prefetch_related' has been applied to a queryset, we need to
        # forcibly invalidate the prefetch cache on the instance.
        instance._prefetched_objects_cache = {}

    return Response(serializer.data)

Lastly, you want to set user on your serializer to read only (or just remove it entirely), as this should always be set based on the credentials passed in the request.

class CustomerSerializer(serializers.ModelSerializer):
    class Meta:
        model = Customer
        fields = ['id', 'user_id', 'profile_image', 'phone']
        read_only_fields = ['user_id']

A great resource for looking at all the functions that you inherit from DRF classes is https://www.cdrf.co/.

Good luck, hope this helps!

CodePudding user response:

Okay, I managed to solve it by overriding the create method in the serializer. I added the following:

class CustomerSerializer(serializers.ModelSerializer):
    class Meta:
        model = Customer
        fields = ['id', 'user_id', 'profile_image', 'phone']
        read_only_fields = ['user_id']

    
    # NEW ---------------------------------
        def create(self, validated_data):
            user = self.context['request'].user
            customer = Customer.objects.filter(user_id=user)
            if customer.exists():
                raise serializers.ValidationError(
                'Customer already exists')
            else:
                customer = Customer.objects.create(
                    user=user, **validated_data)
            return customer

The object saves fine now.

  • Related