[Django]-Django Rest Framework – Updating a foreign key

11👍

As stated in the documentation, you will need to write your own create() and update() methods in your serializer to support writable nested data.

You will also need to explicitly add the status field instead of using the depth argument otherwise I believe it won’t be automatically added to validated_data.

EDIT: Maybe I was a bit short on the details: what you want to do is override update in ModelIssueSerializer. This will basically intercept the PATCH/PUT requests on the serializer level. Then get the new status and assign it to the instance like this:

class StatusSerializer(serializers.ModelSerializer):
    class Meta:
        model = Status

class ModelIssueSerializer(serializers.ModelSerializer):
    status = StatusSerializer()
    # ...
    def update(self, instance, validated_data):
        status = validated_data.pop('status')
        instance.status_id = status.id
        # ... plus any other fields you may want to update
        return instance

The reason I mentioned in the comment that you might need to add a StatusSerializer field is for getting status into validated_data. If I remember correctly, if you only use depth then nested objects might not get serialized inside the update() / create() methods (although I might be mistaken on that). In any case, adding the StatusSerializer field is just the explicit form of using depth=1

👤gbs

2👍

I usually use custom field for such cases.

class StatusField(serializers.Field):

    def to_representation(self, value):
        return StatusSerializer(value).data

    def to_internal_value(self, data):
        try:
            return Status.objects.filter(id=data['id']).first()
        except (AttributeError, KeyError):
            pass

And then in main serializer:

class IssueSerializer(serializers.ModelSerializer):
    status = StatusField()

    class Meta:
        model = MyIssueModel
        fields = (
            'issue_name',
            'status',
        )

1👍

I would assume that your models mimic your serializer’s data. Also, I would assume that you have a one to many relation with the status(es) but you don’t need to create them via the issue serializer, you have a different endpoint for that. In such a case, you might get away with a SlugRelatedField.

from rest_framework import serializers


class StatusSerializer(serializers.ModelSerializer):
    class Meta:
        model = MyStatusModel
        fields = (
            'id',
            'status',
        )


class IssueSerializer(serializers.ModelSerializer):
    status = serializers.SlugRelatedField(slug_field='status', queryset=MyStatusModel.objects.all())

    class Meta:
        model = MyIssueModel
        fields = (
            'issue_name',
            'status',
        )

Another valid solution would be to leave here the foreign key value and deal with the display name on the front-end, via a ui-select or select2 component – the RESTfull approach: you are handling Issue objects which have references to Status objects. In an Angular front-end app, you would query all the statuses from the back-end on a specific route and then you will display the proper descriptive name based on the foreign key value form Issue.

Let me know how is this working out for you.

👤Roba

0👍

Django is a little bit annoying that it ignores relationship fields if depth = 1

One explicit way to deal with this is to define 2 serializers, and define which to use in your Viewset

views:

class MyViewSet(viewsets.ModelViewSet):
    queryset = models.MyModel.objects.order_by("pk")
    serializer_class = serializers.ExpandedSerializer

    def get_serializer_class(self):
        if self.action == ["list", "retrieve"]:
            return serializers.MyExpandedSerializer
        if self.action in ["create", "update", "partial_update"]:
            return serializers.MyCreateSerializer
        return super(LenderOfferingViewSet, self).get_serializer_class()

    def create(self, request, *args, **kwargs):
        """Overload the create method with a different response serializer."""
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        response_serializer = serializers.MyExpandedSerializer(serializer.instance)
        return Response(response_serializer.data, status=status.HTTP_201_CREATED, headers=headers)

    def update(self, request, *args, **kwargs):
        """Overload method for PUT/PATCH to correctly set values and set response serializer."""
        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 = {}

        response_serializer = serializers.MyExpandedSerializer(serializer.instance)
        return Response(response_serializer.data)

And then serializers:


class MyCreateSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.MyObject
        fields = ["issue_name", "status"]
        depth = 0  # no expansion else create/update fail on missing fields (serializer ignores related fields)


class MyExpandedSerializer(serializers.ModelSerializer):
    """This serializer expands related fields explicitly with SerializerMethodFields."""
    class Meta:
        model = models.My
        fields = ["id", "issue_name", "status"]
        depth = 1

Then, when you

POST /my_endpoint/
{
  issue_name: 'This is a problem',
  status: 3
}

You will get the response (on i.e. /my_endpoint/1)

{
  id: 1,
  issue_name: 'This is a problem',
  status: {
    id: 3,
    name: 'todo'
  }
}
👤Roman

Leave a comment