Search code examples
javascriptpythondjangodjango-rest-frameworkdjango-serializer

Django+JS: Can't track likes on posts. Bad request 400


Little background: I'm creating twitter like app from the tutorial on youtube. So, I am a beginner in django and just know syntax of JS.

Several days ago, I faced the problem where likes of a post are not counting. I think the problem is with serializers or with my environment (django 2.2; python 3.8.5; using VS Code).

my action handling view:

import random
from django.conf import settings
from django.http import HttpResponse, Http404, JsonResponse
from django.shortcuts import render, redirect
from django.utils.http import is_safe_url

from rest_framework.authentication import SessionAuthentication
from rest_framework.decorators import api_view, authentication_classes, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from .forms import TweetForm
from .models import Tweet
from .serializers import TweetSerializer, TweetActionSerializer

ALLOWED_HOSTS = settings.ALLOWED_HOSTS

@api_view(['POST'])
@permission_classes([IsAuthenticated])
def tweet_action_view(request, *args, **kwargs):
    '''
    id is required.
    Action options are: like, unlike, retweet
    '''
    serializer = TweetActionSerializer(data=request.data)
    if serializer.is_valid(raise_exception=True):
        data = serializer.validated_data
        tweet_id = data.get("id")
        action = data.get("action")
        content = data.get('content')
        qs = Tweet.objects.filter(id=tweet_id)
        if not qs.exists():
            return Response({}, status=404)
        obj = qs.first()
        if action == "like":
            obj.likes.add(request.user)
            serializer = TweetSerializer(obj)
            return Response(serializer.data, status=200)
        elif action == "unlike":
            obj.likes.remove(request.user)
        elif action == "retweet":
            new_tweet = Tweet.objects.create(
                user=request.user,
                parent=obj,
                content = content)
            serializer = TweetSerializer(new_tweet)
            return Response(serializer.data, status=200)
    return Response({}, status=200)

serializers.py:

from django.conf import settings
from rest_framework import serializers

from .models import Tweet

MAX_TWEET_CHAR = settings.MAX_TWEET_CHAR
TWEET_ACTION_OPTIONS = settings.TWEET_ACTION_OPTIONS

class TweetActionSerializer(serializers.Serializer):
    id = serializers.IntegerField()
    action = serializers.CharField()
    content = serializers.CharField(allow_blank = True, required = False)


    def validate_action(self, value):
        value = value.lower().strip()
        if not value in TWEET_ACTION_OPTIONS:
            raise serializers.ValidationError('This is not a valid action')
        return value


class TweetSerializer(serializers.ModelSerializer):
    likes = serializers.SerializerMethodField(read_only=True)
    class Meta:
        model = Tweet
        fields = ['id', 'content', 'likes']
    
    def get_likes(self, obj):
        return obj.likes.count()

    def validate_content(self, value):
        if len(value) > MAX_TWEET_CHAR:
            raise serializers.ValidationError("You exceeded maximum number of characters (240)")
        return value

home.html :

{% block content %}
<script>
    function tweetActionBtn(tweet_id, currentCount) {
        console.log(tweet_id, currentCount)
        const url = 'api/tweet/action'
        const method = 'POST'
        const data = JSON.stringify({
            id: tweet_id,
            action: 'like'
        })
        const xhr = new XMLHttpRequest()
        const csrftoken = getCookie('csrftoken');
        xhr.open(method, url)
        xhr.setRequestHeader("Content-Type", "application/json")
        xhr.setRequestHeader("HTTP_X_REQUESTED_WITH", "XMLHttpRequest")
        xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest")
        xhr.setRequestHeader("X-CSRFToken", csrftoken)
        xhr.onload = function() {
            loadTweets(tweetsContainerElement)
        }
        console.log(xhr)
        xhr.send()
        return
    }
 
    function UnlikeBtn(tweet) {
        return "<button class='btn btn-outline-primary btn-sm' onclick=tweetActionBtn(" + 
            tweet.id + "," + tweet.likes + ",'unlike')>Unlike</button>"
    }

    function RetweetBtn(tweet) {
        return "<button class='btn btn-outline-success btn-sm' onclick=tweetActionBtn(" + 
            tweet.id + "," + tweet.likes + ",'unlike')>Retweet</button>"
    }

    function LikeBtn(tweet) {
        return "<button class='btn btn-primary btn-sm' onclick=tweetActionBtn(" + 
            tweet.id + "," + tweet.likes + ",'like')>" + tweet.likes + " Likes</button>"
    }


    function formatTweets(tweet) {
        var formattedTweet = "<div class='col-12 col-md-10 mx-auto border rounded py-3 mb-4 tweet' id='tweet-" + tweet.id + "'><p>" + tweet.content +
             "</p><div class='btn-group'>" + 
                LikeBtn(tweet) + 
                UnlikeBtn(tweet) +
                RetweetBtn(tweet) +
                "</div></div>"
        return formattedTweet
    }

    
</script>
{% endblock content %}

url.py :

from django.contrib import admin
from django.urls import path

from tweets.views import (
    home_page,
    tweet_details_view,
    tweet_list_view,
    tweet_create_view,
    tweet_delete_view,
    tweet_action_view,
)

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', home_page),
    path('tweet/<int:tweet_id>', tweet_details_view),
    path('tweets/', tweet_list_view),
    path('create-tweet/', tweet_create_view),
    path('api/tweet/<int:tweet_id>/delete', tweet_delete_view),
    path('api/tweet/action', tweet_action_view),

]

When I click the 'like' or 'unlike' button the number of likes does not increment or decrement. In my console after clicking 'like' button there is following message:

XMLHttpRequest { onreadystatechange: null, readyState: 1, timeout: 0, withCredentials: false, upload: XMLHttpRequestUpload, responseURL: "", status: 0, statusText: "", responseType: "", response: "" }

400 Bad request.

Server sends me the following message:

Bad Request: /api/tweet/action
[09/Jan/2021 16:43:11] "POST /api/tweet/action HTTP/1.1" 400 71

I went to /api/tweet/action and tried to post something and got the following:

HTTP 400 Bad Request
Allow: OPTIONS, POST
Content-Type: application/json
Vary: Accept

{
    "detail": "JSON parse error - Expecting value: line 1 column 1 (char 0)"
}

What can I try next?


Solution

  • You get that error when you are not sending the expected data or data is in wrong format.

    The way you model data determines if the post request needs some data or not. If in your model if some field is marked required=true then it should be available in you post request. In your case it might be action field. Therefore you need to send this data in your post request.

    If you look in your post request using the developer console in your browser you will see that there will be no body in the post request.

    So you should be using xhr.send(data) instead of xhr.send().

    Also you don't need to create an id field for the tweet action it can be handled by Django automatically. By default, Django adds an id field to each model, which is used as the primary key for that model.

    Giving client the ability to send an id is a bad idea, the id should be generated in the server i.e. a post request should not have an id field for the data which you want to be created in the backend.