Thom Zolghadr
Posted on December 13, 2021
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,
})
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 %}
...
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")
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']
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
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
},
]
Posted on December 13, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.