unique_together of two field messes with read_only

2019-09-03 01:38发布

问题:

I have this code for rating lessons, user and lesson should be added automatically from request authorization and URL:

#views.py

class RatingViewSet(
    mixins.ListModelMixin,
    mixins.CreateModelMixin,
    viewsets.GenericViewSet
):
    permission_classes = [permissions.IsAuthenticated]
    serializer_class = RatingSerializer

    def perform_create(self, serializer):
        lessonInstance = Lesson.objects.get(id = self.kwargs['lessonID'])
        serializer.save(user=self.request.user, lesson = lessonInstance)
    def get_queryset(self):
        lessonID = self.kwargs['lessonID']
        return Rating.objects.filter(user=self.request.user, lesson=lessonID)

#serializers.py

class RatingSerializer(serializers.ModelSerializer):
    class Meta:
        model = Rating
        fields = ('id', 'lesson','user', 'difficulty')
        read_only_fields = ('id', 'user','lesson')

#models.py

class Rating(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL)
    lesson = models.ForeignKey('lessons.Lesson')
    difficulty = models.IntegerField()
    class meta:
        unique_together('user','lesson')

I want to have max 1 rating per user/lesson, hence unique_together('user','lesson'). But there is a problem: as long as that constraint is in the code, requests without user or lesson fields get denied with field required error, even though they are read_only.

(If I migrate with unique_together('user','lesson'), then delete that line it works, but as soon as it's there I get errors.)

I want to keep that bit of code there so I don't accidentally remove the unique_together constraint on later migrations.

回答1:

This is a special-case that requires a different approach. Here's what django-rest-framework documentation (see the Note) says about this case:

The right way to deal with this is to specify the field explicitly on the serializer, providing both the read_only=True and default=… keyword arguments.

In your case, you need to explicitly define the user and lesson fields on your RatingSerializer, like this:

class RatingSerializer(serializers.ModelSerializer):
    user = serializers.PrimaryKeyRelatedField(read_only=True, default=serializers.CurrentUserDefault())  # gets the user from request
    lesson = serializers.PrimaryKeyRelatedField(read_only=True, default=None)  # or any other appropriate value

    class Meta:
        model = Rating
        fields = ('id', 'lesson','user', 'difficulty')

Good luck!



回答2:

If a field is read_only=True then the validated_data will ignore data of it => Cause error required field, read more at doc

I also met this issue in a similar context, then tried @iulian's answer above but with no luck! This combo read_only + default behavior is not supported anymore, check this

I resolved this issue by 2 solutions:

  • My model:
class Friendship(TimeStampedModel):
    """Model present Friendship request"""
    from_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='friendship_from_user')
    to_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='friendship_to_user')

    class Meta:
        unique_together = ('from_user', 'to_user')

Solution 1. Write your own CurrentUserDefault class to get the user id then set to default attribute data of serializer(Ref from #51940976)

class CurrentUserDefault(object):
    def set_context(self, serializer_field):
        self.user_id = serializer_field.context['request'].user.id

    def __call__(self):
        return self.user_id

class FriendshipSerializer(serializers.ModelSerializer):
    from_user_id = serializers.HiddenField(default=CurrentUserDefault())

    class Meta:
        model = Friendship
        fields = ('id', 'from_user', 'from_user_id', 'to_user', 'status')
        extra_kwargs = {
            'from_user': {'read_only': True},
        }

Solution 2. Override the create method of serializer to set data for user id(Ref from this)

class FriendshipSerializer(serializers.ModelSerializer):
    class Meta:
        model = Friendship
        fields = ('id', 'from_user', 'to_user', 'status')
        extra_kwargs = {
            'from_user': {'read_only': True},
        }


    def create(self, validated_data):
        """Override create to provide a user via request.user by default.

        This is require since the read_only `user` filed is not included by default anymore since
        https://github.com/encode/django-rest-framework/pull/5886.
        """
        if 'user' not in validated_data:
            validated_data['from_user'] = self.context['request'].user

        return super(FriendshipSerializer, self).create(validated_data)

I hope this helps!