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

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

Last modified on Monday, May 18, 2026 at 3:30 AM

#fullstack development, #macOS, #django, #python3, #fetch api, #manytomany, #function based views, #jsonresponse, #history.go(0)

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

Adding a like button to topic posts

We're finally adding a like button to our topic posts!

Adding a likes field to the Post model

First, we have to add the following to our 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, we have to run python3 makemigrations and then python3 manage.py migrate, because we 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, we can peek into our 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>

We now have a boards_post_likes table. This was created because of our ManyToMany associations.

We 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 we import the get_object_or_404 function from django.shortcuts.

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

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

If the object is found, it is returned. If not, an Http404 exception is raised, triggering a 404 error page.

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 our 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 our application responds appropriately to user requests.

Toggling the like status with an if else statement

Next, we 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 we use the fetch method/api, the returned data contains JSON.

  4. The safe parameter in JsonResponse allows us 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 our views. It handles the serialization process for us, converting Python objects into JSON strings.

In our code, we are 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 --> ... {% block 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 indow history.go() on w3schools.

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.

Conclusion

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

Footnotes

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