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.