How to Return a Boolean Value in Django REST Serializers Based on Related Models

thomz

Thom Zolghadr

Posted on December 13, 2021

How to Return a Boolean Value in Django REST Serializers Based on Related Models

I built an auctions project purely in vanilla Django for CS50 Web. I am now rebuilding this project using Django REST Framework and React to practice building a more robust full stack application. I have been learning a lot about how to use DRF to automate much of the API routing process and really enjoying the power and extensibility. However like all new things you come across snags.

I wanted to be able to conditionally render a heart-outline or filled-heart for each listing if a user was watching/following that particular listing (like a watch list).

You can see from the models.py snippet below that Watching model entries are relational models connecting foreign keys: the user and the listing.

I actually realized while writing this that I never did attempt to solve this problem in vanilla Django, as I had only rendered a watch list status on the individual listing pages:

views.py (vanilla Django)

# additional data removed for clarity
def view_listing(request, id):
    listing = Listing.objects.get(pk=id)
    user = get_user_or_none(request) 

    is_user_watching = Watching.objects.filter(user_id=user, listing_id=listing).exists()

    return render(request,"auctions/listing.html", {  
        "watching": is_user_watching,  
    })

Enter fullscreen mode Exit fullscreen mode

listing.html (Django Template)

...
<form action="{% url 'watchlist' listing.id %}" method="POST">
    {% csrf_token %}
    {% if watching == True %}
    <button> Remove from Watchlist</button>
    {% else %}
    <button> Add to Watchlist</button>
    {% endif %}
</form>
{% else %}
    <a href="{% url 'login' %}">Log In</a> to bid on this auction!
{% endif %}
...
Enter fullscreen mode Exit fullscreen mode

models.py

class Watching(models.Model):
    user_id = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name="watching")
    listing_id = models.ForeignKey(
        Listing, on_delete=models.CASCADE, related_name="watching")
Enter fullscreen mode Exit fullscreen mode

I have watch list status on individual listings, how do I get this for all listings in a single request?

Sometimes the hardest thing can be defining the problem in a manner which helps you to ask the right questions to lead on you the correct path.

Eventually I learned you can get the current user in a serializer method by accessing self.context['request'].user. Now that I had access to the User, I could use that to query the database and see if a related Watching entry exists!

In serializers.py I used Watching.objects.filter().exists() to return True or False if such a follow does indeed exist.

serializers.py

from rest_framework.fields import SerializerMethodField

class ListingSerializer(serializers.ModelSerializer):

# returns true if user is authenticated and a Watching instance  
# exists with this user's id and this listing's id
    def is_watched_by_user(self, instance):
        user_id = self.context['request'].user.id
        listing_id = instance.id
        try:
            return Watching.objects.filter(user_id=user_id, listing_id=listing_id).exists()
        except Exception:
            return False

    creator = serializers.ReadOnlyField(source='creator.username')
    comments = CommentSerializer(many=True, required=False)
    user_is_watching = SerializerMethodField(method_name='is_watched_by_user')

    class Meta:
        model = Listing
        fields = ['creator','comments','user_is_watching']

Enter fullscreen mode Exit fullscreen mode

While writing I thought that the serializer method is_watched_by_user could probably be boiled down a little more by using the related_name user.watching to get a smaller data set. This way we could potentially avoid looking through ALL of the listings, and only look at listings this particular user has a relational database entry for.

updated is_watched_by_user:

def is_watched_by_user(self, instance):
        user_id = self.context['request'].user
        listing_id = instance.id
        try:
            return user.watching.filter(listing_id=listing_id).exists()
        except Exception:
            return False
Enter fullscreen mode Exit fullscreen mode

Now when querying the API, the listings will tell me if the current logged in user is following this listing or not. The Try/Except block assumes that if there is an AttributeError 'attribute Watching does not exist' or Model.DoesNotExist or similar error, then the user is an AnonymousUser or there is no entry. Either way we can return False.

Below is a GET request for a logged in user who follows listing_id 1, but not 2 or 3:

GET ../api/listings/

   "results": [
        {
            "id": 1,
            "creator": "Alice",
            "comments": [],
            "user_is_watching": true
        },
        {
            "id": 2,
            "creator": "Bob",
            "comments": [],
            "user_is_watching": false
        },
        {
            "id": 3,
            "creator": "Charlie",
            "comments": [],
            "user_is_watching": false
        },
    ]
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
thomz
Thom Zolghadr

Posted on December 13, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related