django: How to write JavaScript fetch for url with slug parameter?

Viewed 19

A django and async newbie here, trying to improve a simple message-board app. I'm sure you've all seen this problem dozens of times, but I'm unable to find a solution...

Currently, when a user likes a posted message, it refreshes the whole page. I'd like to use simple JavaScript with the fetch API to prevent this, without having to resort to Ajax, as I've never used it. The problem is, I'm very new to the fetch method as well and I'm struggling to find the correct syntax for the url in the fetch request, as it uses the post model's slug field as a parameter. Like so:

urls.py

urlpatterns = [
    ...
    path('post/<slug:slug>/', views.FullPost.as_view(), name='boards_post'), 
    path('like/<slug:slug>/', views.PostLike.as_view(), name='post_like'),
    ...
]

models.py

...
class Post(models.Model):
    """
    Model for message posts
    """

    STATUS = ((0, "Draft"), (1, "Published"))

    title = models.CharField(max_length=200, unique=True)
    slug = models.SlugField(max_length=200, unique=True)
    author = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name="board_posts"
    )
    category = models.ForeignKey(
        Category,
        on_delete=models.CASCADE,
        default="",
        related_name="category_posts"
    )
    created_on = models.DateTimeField(auto_now_add=True)
    updated_on = models.DateTimeField(auto_now=True)
    content = models.TextField()
    post_image = CloudinaryField('image', default='placeholder')
    status = models.IntegerField(choices=STATUS, default=0)
    likes = models.ManyToManyField(User, related_name="post_likes")

    class Meta:
        # Orders posts in descending order
        ordering = ['-created_on']

    def __str__(self):
        return self.title

    def number_of_likes(self):
        return self.likes.count()

    def get_absolute_url(self):
        """
        Method to tell django how to find the url to any specific
        instance of a post when the Post model is instantiated,
        (i.e. a new post created). Returns the url generated by the
        'boards_post' path from the FullPost class view, with
        this model's slug field as a keyword argument. This
        effectively acts as a redirect to the full_post.html template.
        """

        return reverse('boards_post', kwargs={'slug': self.slug})
...

views.py

...
class FullPost(View):
    """
    View for a single post, selected by the user, displaying
    comments and likes. The url for each individual post is derived
    from the Post model's slug field which is, in turn,
    populated by the title.
    """

    def get(self, request, slug, *args, **kwargs):
        """
        Method to get post object.
        """

        queryset = Post.objects.filter(status=1)
        post = get_object_or_404(queryset, slug=slug)
        comments = post.comments.order_by('created_on')
        liked = False
        if post.likes.filter(id=self.request.user.id).exists():
            liked = True

        return render(
            request,
            "full_post.html",
            {
                "post": post,
                "comments": comments,
                "liked": liked,
                "comment_form": CommentForm() 
            },
        )

    def post(self, request, slug, *args, **kwargs):
        """
        Post method for comment form.
        """

        queryset = Post.objects.filter(status=1)
        post = get_object_or_404(queryset, slug=slug)
        comments = post.comments.order_by("-created_on")
        liked = False
        if post.likes.filter(id=self.request.user.id).exists():
            liked = True

        comment_form = CommentForm(data=request.POST)
        if comment_form.is_valid():
            comment_form.instance.name = self.request.user
            comment = comment_form.save(commit=False)
            comment.post = post
            comment.save()
        else:
            comment_form = CommentForm()

        return redirect(self.request.path_info)
...
...
class PostLike(View):
    """
    View for liking and unliking posts.
    """

    def post(self, request, slug):
        """
        Method to toggle liked/unliked state on a particular post.
        """

        post = get_object_or_404(Post, slug=slug)

        liked = True
        if post.likes.filter(id=self.request.user.id).exists():
            post.likes.remove(request.user)
            liked = False
        else:
            post.likes.add(request.user)

        likes = post.number_of_likes()

        # ----- The original return statement is the one commented out below:
        # return HttpResponseRedirect(reverse('boards_post', args=[slug]))
        return JsonResponse({"likes": likes, "liked": liked})
...

Snippet from full post template

...
<div class="row">
    <div class="col-1">
    {% if user.is_authenticated %}
        <strong>
            <form class="d-inline" action="{% url 'post_like' post.slug %}" method="POST">
            {% csrf_token %}
            {% if liked %}
<!-- I used 'post.id' for the argument passed to the like function here, as I couldn't get 'post.slug' to work -->
                <button class="btn-like" type="submit" name="post_id" value="{{ post.slug }}" onclick="like({{ post.id }})">
                    <i id="like-btn" class="fas fa-thumbs-up"></i>
                </button>
                <span id="likes-count">{{ post.number_of_likes }}</span>
            {% else %}
                <button class="btn-like" type="submit" name="post_id" value="{{ post.slug }}" onclick="like({{ post.id }})">
                    <i id="like-btn" class="far fa-thumbs-up"></i>
                </button>
                <span id="likes-count">{{ post.number_of_likes }}</span>
            {% endif %}
            </form>
        </strong>
    {% else %}
        <strong class="text-secondary"><i class="far fa-thumbs-up"></i> <span id="likes-count">{{ post.number_of_likes }}</span></strong>
    {% endif %}
</div>
...

All I've got so far in JavaScript is the following...

function like(post_id) {
    let likeButton = document.getElementById("like-btn");
    let likeCount = document.getElementById("likes-count");
    
    console.log(likeCount.innerText);
    console.log(post_id);
    
    // -------- I've no idea what works here! I've tried both of the below
    // -------- and several variations.
    // -------- Obviously, none work.
    //   const url = "{%  url 'post_like' post.slug %}";
    //or const url = `/like/${post.slug}`;

    fetch(url, {method: "POST"})
        .then(response => response.json())
        .then(data => {
            console.log(data);
            // ------ If I ever get the json data, I'll use it here to manipulate the
            // ------ likeButton and likeCount variables in the DOM. Something like:
            likeCount.innerHTML = data["likes"];
            if (data["liked"] === true) {
                likeButton.className = "fas fa-thumbs-up";
            } else {
                likeButton.className = "far fa-thumbs-up";
            }
        })
        .catch((e) => alert("Unable to like/unlike post."));
        // -------- or something.
}

Also, I know I need to handle the csrf token somehow but I've no idea how. Like I said, total newbie. So, can anyone help? And does anyone have any advice? Cheers!

1 Answers

For csrf_token problem add to view this decorator:

from django.utils.decorators import method_decorator

@method_decorator(csrf_exempt, name='dispatch')
class PostLike(View):
    ...

For fetch you need to have whole path, not just relative path. This should work:

url = "{{ request.scheme }}://{{ request.get_host }}{% url 'post_like' post.slug %}"
Related