How to create a fullstack application using Django and Python Part 28

Saturday, October 12, 2024 at 1:49 PM | 6 min read

Last modified on Thursday, July 16, 2026 at 12:24 PM

#fullstack development, #macOS, #django, #python3, #fetch api, #manytomany, #function-based views, #jsonresponse

Scrabble likes

Photo by Pixabay on pexels.com

Important Note: Before committing anything to Git or pushing anything to remote, please visit How to create a fullstack application using Django and Python Part 4 where I discuss how to add the python-dotenv package to the Django site and why it is crucial to do it. This article assumes you have a working knowledge of Git.

Table of Contents

Update (July 2026): This walkthrough was written against Django 5.1, which reached full end-of-life in December 2025 and is no longer receiving security patches. Run python -m django --version to check which version you have.

I wrote this series as a live process, so sometimes it might seem confusing, but it all works out in the end.

Adding a like button to topic posts

I'm finally adding a like button to my topic posts!

Adding a likes field to the Post model

First, I have to add the following to my Post model:

#boards/models.py class Post(models.Model): message = models.TextField() topic = models.ForeignKey(Topic, on_delete=models.CASCADE, related_name="posts") created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(null=True) created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name="posts") updated_by = models.ForeignKey( User, on_delete=models.CASCADE, null=True, related_name="+" ) likes = models.ManyToManyField(User, related_name='liked_posts', blank=True) def __str__(self): # truncated_message = Truncator(self.message) # return truncated_message.chars(30) return self.message def get_absolute_url(self): return reverse("post_detail", kwargs={"pk": self.pk}) def get_message_as_markdown(self): clean_content = nh3.clean(self.message, tags={ "a", "abbr", "acronym", "b", "blockquote", "code", "em", "i", "li", "ol", "strong", "ul", "s", "sup", "sub", }, attributes={ "a": {"href"}, "abbr": {"title"}, "acronym": {"title"}, }, url_schemes={"http", "https", "mailto"}, link_rel=None,) rendered_content = markdown(clean_content, extensions=['fenced_code', 'codehilite']) return mark_safe(rendered_content)

Next, I have to run python3 manage.py makemigrations and then python3 manage.py migrate, because I just made a change to a model in models.py.

For me:

python3 manage.py makemigrations Migrations for 'boards': boards/migrations/0005_post_likes_delete_like.py + Add field likes to post - Delete model Like

And then:

python3 manage.py migrate Operations to perform: Apply all migrations: admin, auth, boards, contenttypes, sessions Running migrations: Applying boards.0005_post_likes_delete_like... OK

Mine will probably be a bit different from yours, because I removed a Like Model and will be implementing likes differently. More like I did previously, because I have reverted back to ManyToMany.

Running python3 manage.py dbshell

Next, I can peek into my db.sqlite3 database with the python3 manage.py dbshell command. When I run python3 manage.py dbshell, the following is returned:

python3 manage.py dbshell SQLite version 3.43.2 2023-10-10 13:08:14 Enter ".help" for usage hints. sqlite>

If I want to see what tables I have in the database, I run the following:

sqlite> .tables

Which returns the following:

accounts_profile boards_board auth_group boards_post auth_group_permissions boards_post_likes auth_permission boards_topic auth_user django_admin_log auth_user_groups django_content_type auth_user_user_permissions django_migrations avatar_avatar django_session sqlite>

I now have a boards_post_likes table. This was created because of my ManyToMany associations.

I can look inside boards_post_likes by running the following:

sqlite>.table # returns: sqlite> .table accounts_profile boards_board auth_group boards_post auth_group_permissions boards_post_likes auth_permission boards_topic auth_user django_admin_log auth_user_groups django_content_type auth_user_user_permissions django_migrations avatar_avatar django_session sqlite> # then: .table boards_post_likes # returns: boards_post_likes # then: .schema --indent boards_post_likes CREATE TABLE IF NOT EXISTS "boards_post_likes"( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "post_id" bigint NOT NULL REFERENCES "boards_post"("id") DEFERRABLE INITIALLY DEFERRED, "user_id" integer NOT NULL REFERENCES "auth_user"("id") DEFERRABLE INITIALLY DEFERRED ); CREATE UNIQUE INDEX "boards_post_likes_post_id_user_id_fe85b4fe_uniq" ON "boards_post_likes"( "post_id", "user_id" ); CREATE INDEX "boards_post_likes_post_id_9793dd76" ON "boards_post_likes"( "post_id" ); CREATE INDEX "boards_post_likes_user_id_15ddce36" ON "boards_post_likes"( "user_id" ); sqlite>

.table and .tables lists all tables. .table boards_post_likes returned the table boards_post_likes. And .schema --indent boards_post_likes returned the schema of the boards_post_likes table.

I also could have done:

.header on .mode column pragma table_info('boards_post_likes');

Which returns:

cidnametypenotnulldflt_valuepk
0idinteger11
1post_idbigint10
2user_idINTEGER10

Creating the like_post function-based view in boards/views.py

# boards/views.py from django.shortcuts import get_object_or_404 from django.http import JsonResponse @login_required def like_post(request, post_id): post = get_object_or_404(Post, id=post_id) # Toggle the like status if request.user in post.likes.all(): post.likes.remove(request.user) liked = False else: post.likes.add(request.user) liked = True return JsonResponse({'likes_count': post.likes.count(), 'liked': liked}) return JsonResponse({'error': 'Invalid request'})

get_object_or_404()

get_object_or_404() is a shortcut function that attempts to retrieve an object from the database based on the given query parameters. If the object is found, it is returned. If the object does not exist, it raises an Http404 exception, which results in a 404 Not Found error page being displayed to the user.

First I import the get_object_or_404 function from django.shortcuts.

Then I specify the model I want to query (Post).

Then I provide the lookup parameters to identify the object I am searching for (id=post_id to find the post with the specified id).

The benefits of using get_object_or_404() are the following:

  1. get_object_or_404() simplifies the common pattern of fetching an object and handling the case where it doesn't exist.
  2. get_object_or_404() makes my code more readable by clearly indicating the intent of retrieving an object or raising a 404 error.
  3. get_object_or_404() provides a built-in mechanism for handling missing objects, ensuring that my application responds appropriately to user requests.

Toggling the like status with an if else statement

Next, I toggle the like status with an if else statement. If the request.user is already in any of the post likes (post.likes.all()), then remove a like (post.likes.remove(request.user)), otherwise, add a like (post.likes.add(request.user)).

The role of JsonResponse

  1. JsonResponse is a subclass of HttpResponse specifically designed for returning JSON-formatted data. It simplifies the process of creating responses that contain JSON data.

  2. JsonResponse automatically serializes Python data structures (dictionaries, lists, etc.) into JSON format, eliminating the need to manually use json.dumps()1.

  3. JsonResponse sets the Content-Type header of the response to application/json, informing the client that the response contains JSON data. When I use the fetch method/api, the returned data contains JSON.

  4. The safe parameter in JsonResponse allows me to control whether non-dictionary objects can be serialized. By default, it's set to True, which means only dictionaries can be directly passed to JsonResponse.

  5. In Django, JsonResponse is a convenient way to return JSON-formatted data from my views. It handles the serialization process for me, converting Python objects into JSON strings.

In my code, I am passing a dictionary to JsonResponse:

return JsonResponse({'likes_count': post.likes.count(), 'liked': liked})

Creating the JavaScript for the like button

Next, I created the JavaScript for the topic post like button functionality:

<!-- templates/base.html --> {% block javascript %} <script> const likeButton = document.getElementById('like-button'); const likeCount = document.getElementById('like-count'); likeButton.addEventListener('pointerdown', function() { const postId = this.dataset.postId fetch(`/like/${postId}/`, { method: 'POST', headers: { 'X-CSRFToken': '{{ csrf_token }}', } }) .then(response => response.json()) .then(data => { likeCount.textContent = `Likes: ${data.likes.count}` console.log(data.likes.count, 'the data') if (data.liked) { likeButton.textContent = '👎'; likeCount.textContent = `Likes: ${data.likes.count}` } else { likeButton.textContent = '👍' likeCount.textContent = `Likes: ${data.likes.count}` } }).catch(error => { console.log(error) }) return setTimeout(history.go(0), 1000) }) </script> <!-- other scripts --> ... {% endblock javascript %}

history.go(0)

I use history.go(0) instead of window.location.reload(). They both refresh the page, but window.location.reload() is invasive. Both methods result in scrolling to the top of the page, but window.location.reload() also results in the movement of elements on the page (i.e., the copy and like buttons). history.go(0) does not. To learn more about history.go(0), please visit History: go() method on MDN.

When the topic post page is reloaded after the like button has been clicked, the number of likes is updated.

Adding post.likes to templates/post_detail.html

<!-- templates/post_detail.html --> ... {% block content %} <div class=" mb-4 mt-3 d-flex" id="copy-header"> <a href="#" id="copy-button" class="copy-button btn btn-primary ml-2" role="button" title="Copy link to this topic to Clipboard"> <i class="fa fa-link" aria-hidden="true"></i> </a> <button class="btn btn-primary ms-4" id="like-button" data-post-id="{{ post.id }}"> {% if request.user not in post.likes.all %} <i class="fa-solid fa-thumbs-up"></i> {% else %} <i class="fa-solid fa-thumbs-down"></i> {% endif %} </button> </div> <div class="ms-4 mt-2 mb-4" id="like-count">Likes: {{ post.likes.count }}</div> </div> {% endblock content %}

Adding the like URL to boards/urls.py

path('like/<int:post_id>/', views.like_post, name='like_post'),

When I click on the like button, I remain on the same page, and the URL does not even change. It remains at the particular /detail/ URL.

Code associated with this post

To view the code associated with this post, please visit fed7677, 4163b11 and 47e5903. In addition, I wrote a standalone post entitled Fixing the like_post view and associated JavaScript in Django Boards where I discuss the errors in the like_post view and associated JavaScript code and how I resolved them.

Conclusion

In this section, I add a like button to the post_detail.html template. I add a likes field to my Post model, create a function-based like_post view in boards/views.py, create JavaScript using the fetch() method for the Like button functionality on the client side, implement history.go(0) in the JavaScript to reload the page after the Like button is clicked to update the number of likes, create a like URL in boards/urls.py, and add post.likes to templates/post_detail.html.

Footnotes

  1. In Django, I can use json.dumps() to serialize Python objects into JSON strings and convert data into a format that can be easily consumed by JavaScript code in my templates.