listboss
listboss

Reputation: 886

How to validate together-uniqueness of two (foreign key) related fields of a model?

Let's say I have three models in my app that lets user write a review for a book. The requirement is that every user can only do one review for each book.

Review model has two foreign keys to User and Book models. I want to be able to validate user input before creating an instance of Review model to ensure a user can not create more than one review per book.

I know how to do it if I ask the user to provide the User and Book information in the data that's sent to DRF version 3.+.

But the url to post a new Review has the Book id in it, and the user is authenticated:

url used to list and create reviews: /book/{book_id}/reviews/

Right now when I do a POST operation on the url, DRF complains that the book field is required, even though I tried to send keyword arguments to the overloaded perform_create function (see snippets below)

My guess is I should send the information about book and review that's embedded in the url and request to the default UniqueTogetherValidator but I don't know how to do it!

I have seen a partial solution in this question, but my problem is that I not only have to provide the current authenticated user to the validator but also the book information, since I am requiring a unique_together constraint.

So my question is how to validate unique_together of input data from user's request when the book information is embedded in the url and the user information is part of the incoming request (while both of those are related foreign keys and not local attributes of Review model)?

Here are my models and serializers:

models.py

class Review(models.Model):

    # Relationships
    book = models.ForeignKey(Book)
    user = models.ForeignKey(User)
    comment = models.TextField(null=True, blank=True)

    class Meta:
        unique_together = (("user", "book"),)

serializers.py

class ReviewSerializer(serializers.ModelSerializer):

    book = serializers.PrimaryKeyRelatedField(read_only=True)
    user = serializers.PrimaryKeyRelatedField(read_only=True)

    class Meta:
        model = Review

Part of views.py

class ReviewList(generics.ListCreateAPIView):
    serializer_class = ReviewSerializer
    def perform_create(self, serializer):
        serializer.save(user=self.request.user, book=self.kwargs['book_id'])

urls.py

url(r'^book/(?P<book_id>\d+)/reviews/$', view=ReviewList.as_view(), name='review-list')

Upvotes: 5

Views: 2113

Answers (1)

Linovia
Linovia

Reputation: 20996

You are trying to make things harder than they should.

My feeling on this issue is that the unique together constraint is part of the business rules. Therefore I'd go with removing that constraint on the serializer. After the serializer has validated the data you'll be able to check the constraint on the review (owner + book) and raise a validation error if there's already one. Doing this in the perform_create before saving the serializer seems sensible.

To remove the unique together constraint you'll have to explicitly set the validators on the serializer:

class ReviewSerializer(serializers.ModelSerializer):

    book = serializers.PrimaryKeyRelatedField(read_only=True)
    user = serializers.PrimaryKeyRelatedField(read_only=True)

    class Meta:
        model = Review
        validators = []

Make sure to print a serializer instance before doing that change so you can make sure you're not removing other contraints.

Upvotes: 2

Related Questions