A.Mohammadi
A.Mohammadi

Reputation: 364

How to auto populate a read-only serializer field in django rest framework?

I have a question regarding django rest framework.

Most of the time, I have a serializer which has some read-only fields. For example, consider this simple model below:

class PersonalMessage(models.Model):
    sender = models.ForeignKey(User, related_name="sent_messages", ...)
    recipient = models.ForeignKey(User, related_name="recieved_messages", ...)
    text = models.CharField(...)

    def __str__(self) -> str:
        return f"{self.text} (sender={self.sender})"

In this model, the value of sender and recipient should be automatically provided by the application itself and the user shouldn't be able to edit those fields. Alright, now take a look at this serializer:

class PersonalMessageSerializer(serializers.ModelSerializer):
    class Meta:
        model = PersonalMessage
        fields = '__all__'
        read_only_fields = ('sender', 'recipient')

It perfectly prevents users from setting an arbitrary value on the sender and recipient fields. But the problem is, when these fields are marked as read-only in the serializer, the serializer will completely ignore all the values that are passed into the constructor for these fields. So when I try to create a model, no values would be set for these fields:

PersonalMessageSerializer(data={**request.data, 'sender': ..., 'recipient': ...) # Won't work

What's the best way to prevent users from setting an arbitrary value and at the same time auto-populate those restricted fields in django rest framework?

Upvotes: 1

Views: 2049

Answers (4)

Dr Manhattan
Dr Manhattan

Reputation: 14037

So this is how you can make set a default on create but read only after in DRF. Although in this solution it wont actually be readonly, it's writable, but you now have explicit control on what the logged in user can write, which is the ultimate goal

Given the model

class PersonalMessage(models.Model):
    sender = models.ForeignKey(User,...)
    recipient = models.ForeignKey(User,..)
    text = models.CharField(...)

You would first create your own custom default (I will show an example for only one field)

# Note DRF already has a CurrentUserDefault you can also use
class CurrentSenderDefault:
    requires_context = True

    def __call__(self, serializer_field):
        return serializer_field.context['request'].user

    def __repr__(self):
        return '%s()' % self.__class__.__name__

Next you make your own field, that knows whats up with the filter. This queryset prevents people from setting a value they are not allowed to. which is exactly what you want

class SenderField(serializers.PrimaryKeyRelatedField):

    def get_queryset(self):
        user = self.context['request'].user
        if user:
            queryset = User.objects.filter(id=user.id)
        else:
            queryset = User.objects.none()
        return queryset

Finally on the serialiser you go

class PersonalMessageSerializer(serializers.ModelSerializer):
    
    sender = SenderField(default=CurrentSenderDefault())
    recipient = ...

    class Meta:
        model = PersonalMessage
        fields = '__all__'
        read_only_fields = ('sender', 'recipient')

Upvotes: 0

gripep
gripep

Reputation: 379

From the question it is not possible to understand what field(s) of the relationship with sender and recipient you want to interact with, but a general answer can be found in the Serializer relations section of Django REST documentation.

Long story short, if you want to interact with one field only, you can use SlugRelatedField, which lets you interact with the target of the relationship using only one of its fields. If it just the id, you can use PrimaryKeyRelatedField.

If you want to interact with more than one field, the way to go is Nested Relationships. Here you can specify a custom serializer for the target relationship, but you will have to override the create() method in your PersonalMessageSerializer to create the object from your relationship, as nested serializers are read-only by default.

Upvotes: 0

jackquin
jackquin

Reputation: 574

You able to override the serializer context like this;

PersonalMessageSerializer(data={**request.data, context={'sender': sender, 'recipent': recipent})

and catch the context inside serializer.

class PersonalMessageSerializer(serializers.ModelSerializer):
    class Meta:
        model = PersonalMessage
        fields = '__all__'
        read_only_fields = ('sender', 'recipient')
    def validate(self, attrs):
        attrs = super().validate(attrs)
        attrs['sender'] = self.context['sender']
        attrs['recipent'] = self.context['recipent']
        return attrs

now serializer.validated_data it must returns sender and recipent.

Upvotes: 1

Brian Destura
Brian Destura

Reputation: 12068

Depending on how you get those two objects, you can use the serializer's save method to pass them, and they will automatically be applied to the object you are saving:

sender = User.objects.first()
recipient = User.objects.last()

serializer = PersonalMessageSerializer(data=request.data)
message = serializer.save(sender=sender, recipient=recipient)

The kwargs should match the field names in your model for this to work. For reference, have a look here

Upvotes: 1

Related Questions