Home > Software design >  Django REST Framework UniqueTogetherValidator fails with FK from kwargs
Django REST Framework UniqueTogetherValidator fails with FK from kwargs

Time:07-05

I've got myself into a trouble with UniqueTogetherValidator.

The problem is that ReviewSerliazer unlike CommentSerializer which is almost identical does unique together validation before actually getting a title value from kwargs and sends back 400 answer with title field being required. I've tried to identify it as a HiddenField, but although serializer validation goes fine within tests, model validation does not. And I receive django.db.utils.IntegrityError: UNIQUE constraint failed: reviews_review.author_id, reviews_review.title_id

Key idea is that title should not be included in serializer response. Making it PrimaryKeyRelatedField and then doing the to_representation seems odd to me.

Any ideas how to fix this without catching exceptions in the viewset, which is obviously wrong?

models.py

class BasePost(models.Model):
    text = models.TextField()
    author = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name='%(class)ss')
    pub_date = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ('-pub_date', )
        abstract = True

    def __str__(self):
        return self.text[:30]


class Review(BasePost):
    score = models.IntegerField(
        default=0, validators=[MaxValueValidator(10), MinValueValidator(1)])
    title = models.ForeignKey(
        Title, on_delete=models.CASCADE, related_name='reviews')

    class Meta(BasePost.Meta):
        constraints = [
            models.UniqueConstraint(
                fields=('author', 'title', ), name='unique_title_review')]


class Comment(BasePost):
    review = models.ForeignKey(
        Review, on_delete=models.CASCADE, related_name='comments')

urls.py

router_v1.register(
    r'^titles/(?P<title_id>\d )/reviews', ReviewViewSet, basename='review')
router_v1.register(
    r'^titles/(?P<title_id>\d )/reviews/(?P<review_id>\d )/comments',
    CommentViewSet, basename='comment')

views.py

class ReviewViewSet(BasePostViewSet):
    serializer_class = ReviewSerializer

    def get_queryset(self):
        return self.get_title().reviews.all()

    def perform_create(self, serializer):
        serializer.save(author=self.request.user, title=self.get_title())

    def get_title(self, key='title_id'):
        return get_object_or_404(Title, id=self.kwargs.get(key))


class CommentViewSet(BasePostViewSet):
    serializer_class = CommentSerializer

    def get_queryset(self):
        return self.get_review().comments.all()

    def perform_create(self, serializer):
        serializer.save(author=self.request.user, review=self.get_review())

    def get_review(self, key='review_id'):
        return get_object_or_404(Review, id=self.kwargs.get(key))

serializers.py

class ReviewSerializer(BasePostSerializer):
    title = serializers.HiddenField(default=None)

    class Meta:
        model = Review
        fields = ('id', 'text', 'author', 'score', 'pub_date', 'title', )
        validators = [UniqueTogetherValidator(
            queryset=Review.objects.all(), fields=('author', 'title', ))]


class CommentSerializer(BasePostSerializer):

    class Meta:
        model = Comment
        fields = ('id', 'text', 'author', 'pub_date', )

CodePudding user response:

def create(self, validated_data):
    try:
        review = Review.objects.create(**validated_data)
    except IntegrityError:
        raise serializers.ValidationError(
            {'detail': 'Вы можете оставить только один отзыв.'})
    return review

Currently fixed like that but seems wrong to me.

CodePudding user response:

First, I will assume that BasePostViewSet does inherits from CreateModelMixin. My assumption is based on the fact that you are redefining perform_create:

def perform_create(self, serializer):
    serializer.save()

According to DRF Documentation on passing additional argumernts to the .save() method:

Any additional keyword arguments will be included in the validated_data argument when .create() or .update() are called.

The original .create() method from the CreateModelMixin:

def create(self, request, *args, **kwargs):
    serializer = self.get_serializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    self.perform_create(serializer)
    headers = self.get_success_headers(serializer.data)
    return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

It does the validation before calling the .perform_create() method, hence your redefined version of .perform_create:

def perform_create(self, serializer):
    serializer.save(author=self.request.user, review=self.get_review())

Gets called after validation, hence, your serializer validator is not checked with your desired arguments.

Now, I think you could achieve this by including your fields in the to_internal_value function data like this (didn't try it out but definitely it runs before validation) (I'm assuming that serializer context is passed accordingly):

def to_internal_value(self, data):
    data['author'] = self.context['request'].user
    data['review'] = self.context['view'].get_review()
    return data

Hopefully this helps.

  • Related