Django REST Framework UniqueTogetherValidator не работает с FK из kwargs

Я вляпался в неприятности с UniqueTogetherValidator.

Проблема в том, что ReviewSerliazer в отличие от CommentSerializer, который почти идентичен, делает уникальную совместную валидацию перед фактическим получением значения title из kwargs и отправляет обратно 400 ответов с обязательным полем title. Я пытался идентифицировать его как HiddenField, но хотя валидация сериализатора проходит нормально в тестах, валидация модели не проходит. И я получаю django.db.utils.IntegrityError: UNIQUE constraint failed: reviews_review.author_id, reviews_review.title_id

Ключевая идея заключается в том, что заголовок не должен включаться в ответ сериализатора. Делать его PrimaryKeyRelatedField и затем делать to_representation кажется мне странным.

Есть идеи, как исправить это без перехвата исключений в наборе представлений, что, очевидно, неправильно?

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

В настоящее время исправлено так, но мне кажется, что это неправильно.

Во-первых, я предположу, что BasePostViewSet действительно наследует от CreateModelMixin. Мое предположение основано на том, что вы переопределяете perform_create:

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

Согласно DRF документации о передаче дополнительных аргументов методу .save():

Дополнительные аргументы ключевых слов будут включены в аргумент validated_data при вызове .create() или .update().

Оригинальный метод .create() из 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)

Он выполняет проверку перед вызовом метода .perform_create(), поэтому ваша переопределенная версия .perform_create:

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

Вызывается после валидации, следовательно, ваш валидатор сериализатора не проверяется с нужными вам аргументами.

Теперь, я думаю, вы можете достичь этого, включив ваши поля в данные функции to_internal_value следующим образом (не пробовал, но определенно это запускается перед валидацией) (я предполагаю, что контекст сериализатора передается соответствующим образом):

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

Надеюсь, это поможет.

Вернуться на верх