top of page
Search
  • Writer's pictureRiya Manchanda

How to Make Pagination in Django using JavaScript



Python and JavaScript are undoubtedly two of the most popular web programming languages in today's world. So, in an attempt to learn, I had recently been working on a project as part of a HarvardX course called, 'CS50's Web Programming with Python and JavaScript'. This project required me to developer a Twitter-like Social Media platform which allows users to make posts and view posts made by everyone else.


Another requirement of this project was to display the posts in separate pages, which is slightly more tricky than one would imagine. So, I though I would write about and help you guys out if you're struggling with the same!


Table of Contents:

  1. What is Django?

  2. Introduction to Pagination

  3. Creating a Basic Project

  4. Fetching Posts

  5. Using Django Paginator Class

  6. Fetching number of Pages

  7. Completing Pagination



What is Django?


For those who are new to Web Programming with Python and JavaScript, Django is an open-sourced web framework written in Python. Its simple and efficient structure allows developers create smooth, maintainable and secure dynamic websites.


Django provides numerous tools, features, functions, models and classes which reduce the amount of time developers have to spend on repetitive components in a web application, instead giving them more freedom to work on the other authentic features.


That was a basic introduction to Django, we will not be diving into a lot of detail there. However, before we begin, there are some basic things that you should know, such that it uses a template-view-model structure.


Templates are our HTML files (controlled by JavaScript and CSS scripts in our case) which display on the website, Views are Python functions control the function, routing and the API of our application, and Models are our data Objects for storing data within a sqlite database.


Before we move forward, let us quickly create a basic Django project using our command line (or terminal). We are assuming here that you have already installed the latest version of Django in your system. If so, enter the following line there:


django-admin startproject project

I will be calling my project 'project', you can name it whatever you wish. Great, so now we have a project, or an environment ready for our Application. Within this project, we will now develop an application by the following command in our project directory, referencing our manage.py file in our project, which manages all our apps:


python manage.py startapp network

Awesome! Now we have our app ready, you should see a directory within your project with the name of your app. Within this app folder you will see the following files that we will be working with: views.py and models.py. You should also create a urls.py file here and create a urlpatterns array there, just like in the urls.py of our project. We will also be making some folders of our own later. At this point you should also register your app's urls in your projects urls.py file by adding the following line of code in the urlpatterns file:


path('', include('network.urls'))

Don't forget 'include'. The last thing we need to know before moving on to the next section, is how to run a Django Project. Go into the app directory and run the following:


python manage.py runserver

Now you can enter your root local host url on your browser (if it doesn't redirect you automatically) and view your project. We're off to a good start, but things are about to get technical now!



Introduction to Pagination


Before diving into our project, let us take a quick glance at what Pagination is. In simple words, Pagination is when you separate data or content in separate webpages instead of displaying an endless list. And that is exactly what we will doing.



We will use JavaScript and Python Django to build this functionality in our app with some dummy data, or social media posts. We will be adding buttons 'Next' and 'Previous' buttons.





Above is a brief overview of the kind of functionality our final project will be demonstrating. Now that we are clear with this, let us get straight to business!



Beginning Our Project


So, the first step before we dive into our pagination feature, we need to create a basic structure for our project to work with. Let us start with our HTML templates. For the sake of quality and convention, I will start by creating a layout for my HTML templates to extend:



{% load static %}

<!DOCTYPE html>
<html lang="en">
 	<head>
 	    <title>{% block title %}Social Network{% endblock %}</title>
 	    <link rel="stylesheet" 		href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">

 	    <link href="{% static 'network/styles.css' %}" rel="stylesheet">
 	    <script src="{% static 'network/network.js' %}" />
 	</head>
 	<body>

 	    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
 		<a class="navbar-brand" href="#">Network</a>
 
 		    <div class="btn-group"> 
                	{% if user.is_authenticated %}
 			    <button class="btn btn-light user-link" id="profile" value="{{ user.username }}">{{ user.username }}</button>
                	{% endif %}
 			    <button class="btn btn-warning" id="all">All Posts</button>
                	{% if user.is_authenticated %}
 			    <button class="btn btn-warning" id="following">Following</button>
 			    <a class="btn btn-warning" href="{% url 'logout' %}">Log Out</a>
                	{% else %}
 			    <a class="btn btn-warning" href="{% url 'login' %}">Log In</a>
 			    <a class="btn btn-warning" href="{% url 'register' %}">Register</a>
                	{% endif %}
 		    </div>
 		</nav>
 
 		<div class="body">
            	    {% block body %}
                    {% endblock %}
 		</div>
 	</body>
</html>


Here I have basically used BootStrap stylesheets to create a navigation bar for my entire project, since I am creating a prototype Social Media Application. You can skip all of this and simply create a basic template if you want as follows.


{% extends "network/layout.html" %}

{% block body %}
<div id="main-view">
	// This is where our posts will come through JavaScript 
</div>

<nav aria-label="Page navigation example">
 	<ul class="pagination justify-content-center" id="page-number">
 		// This is where our pagination buttons will come through JavaScript
 	</ul>
 </nav>
{% endblock %}

So essentially, I have created basic divisions in my HTML template where I will append my posts' HTML through JavaScript later. The same I have done for my pagination buttons, using BootStrap styling. Next we will create a view to display our template when that particular Python function is called as follows. I will also be importing some Modules which I will use later on.



from django.db import IntegrityError
from django.http import HttpResponse, JsonResponse
from django.shortcuts import render
from django.core.paginator import Paginator

import json

def index(request):
    return render(request, "network/index.html")
    

Great going, now we need to go ahead and call this function whenever the registered 'index' url is requested. We should add the following code to our urls.py file within our application directory (not our project one!):



from django.urls import path

from . import views

urlpatterns = [
	path("", views.index, name="index")
]


Awesome, so now every time somebody visits our root localhost url, they will see our index.html file. Now you can probably run your server and check out the page you made and fix any problems that might be occurring so far. It should look something like this:



The next step, is to create a Model structure for our posts which we want to display in our template and paginate. I essentially want to include the following attributes in my Post model: username, content, timestamp and likes. I can also create a User Model using Django's pre-existing abstractUser Model, such that we can interconnect both our models:



from django.contrib.auth import abstractUser
from django.db import models

class User(AbstractUser):
	pass

class Post(models.Model):
    user = models.ForeignKey("User", on_delete=models.CASCADE, related_names="posts")
    content = models.TextField(blank=True)
    timestamp = models.DateTimeField(auto_now_add=True)
    likes = models.ManyToManyField("User", related_name="liked_posts", null=True)

    def serialize(self):
	return {
	    "id": self.id, 
	    "username": self.user.username,
	    "content": self.content,
	    "timestamp": self.timestamp.strftime("%b %-d %Y, %-I:%M %p"),
	    "likes": [like.username for like in self.likes.all()]
	}



One thing to note here is, that I have written a serialize function inside of my model, which is primarily to structure it in JSON format when fetching it through JavaScript. At this point you should also make your migrations using the following commands:


python manage.py makemigrations
python manage.py makemigrations

Now, that we have our Models ready, we better import them to our views.py file for use in the future components:


from .models import *

Bravo, you're probably feeling good about your work by now. The next step, which is to fetch posts and display them, requires us to create some data in our python console in our terminal window. You can run the python console in your terminal by:


python manage.py shell

Then you can run the following commands to create new posts:



from network.models import *
user = User(username: "username", password: "password")
user.save()
post = Post(user: user, content: "First Post!")
post.save()



And you can create multiple such posts, (preferably around 15 for our purpose ) and experiment with it however you like. Once that is through, we can now make our first API call using JavaScript and fetch our dummy posts into our HTML template.



Fetching Posts


Good work so far, now we can get started on the JavaScript part of things, and fetch posts into our User Interface. For this what we want to do is, write an API function in our views.py, which sends our posts data as a JSON response to our JavaScript front-end to display. So the function should go like this:



def posts (request):

    posts = Post.objects.all()
    posts = posts.order_by("-timestamp").all()

    return JsonResponse([post.serialize() for post in posts], safe=False)


Here we are ordering all our posts by time and sending them over. Setting safe to false means any data type can be sent as JSON Response as opposed to only dictionaries. Now we can register this url in urls.py:



from django.urls import path

from . import views

urlpatterns = [
	path("", views.index, name="index"),
	path("posts/<str:endpoint>", views.posts, name="posts")
]


Cool, so now we can send a get request through a JavaScript to fetch our posts:



function fetch_post() {
    document.querySelector('#main-section').innerHTML = ''
    fetch(`/posts`)
    .then(response => response.json())
    .then(posts => {
        posts.forEach(element => {
                let post = document.createElement('div')
                post.innerHTML = //post HTML code here!
               document.querySelector('#main-section').append(post)
                post.classList.add("box")
                post.id = element.id
        })
    }
}


Here I have fetched my post details, and created a div tag for each of them. Then I add a couple classes to my div tag as per the Bootstrap styling, which can be ignored if you wish. Below you can find my HTML for the post's innerHTML:



post.innerHTML = 
`<span>
     <button class="btn btn-link user-link"> 
         ${element.username}
     </button>
 </span>
 <button class="btn btn-outline-info edit"> 
     Edit
 </button>
 <hr>
 <p> ${element.content} </p>
 <p class="card-subtitle mb-2 text-muted"> 
     ${element.timestamp} 
 </p>
 <button class="btn like">`
     <svg xmls="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-hear-fill like" viewBox="0 0 16 16">
         <path fill-rule="evenodd" d="M8 1.314C12.438-3.248 23.534 4.735 8 15-7.534 4.736 3.562-3.248 8 1.314z"/>
     </svg>
 </button>
<span class="margin"> 
     ${element.likes.length} likes
</span>


At this point you can call this function within a JavaScript Event Listener for your document and test whether your posts are displayed:



document.addEventListener('DOMContentLoaded', function () {

	document.querySelector('#main-view').style.display = 'block';
	fetch_post()

})


The result should appear something like this:



Great going, we are now actually ready to begin working on our Pagination (phew, finally!), or in other words, "It's show time!". We will now jump right into pagination, and begin by restructuring our Python function a little bit.



Using Django's Paginator Class


For implementing Pagination on our back-end / server side, Django luckily provides us with a shortcut method to divide our data instances into different groups/pages based on our specified number of objects per page. Pretty neat, no?


This method is the Django Paginator class, which provides use with various functionalities ranging from listing all the objects of a page, to determining the total number of pages. So as you must have seen before, we have already imported Django's paginator class in the views.py file. Now we can go ahead and make the following changes to our posts function:



def posts (request, endpoint):

    posts = Post.objects.all()
    posts = posts.order_by("-timestamp").all()
    paginator = Paginator(posts, 5)
    counter = int(request.GET.get("page") or 1)

    if endpoint == "posts":
        page = paginator.page(counter)
        set_posts = page.object_list

        return JsonResponse([post.serialize() for post in set_posts], safe=False)

    else if endpoint == "pages":
        return JsonResponse({"pages": paginator.num_pages})

    else:
        return HttpResponse(status=404)


Okay, so let me explain what just happened here. Firstly, we added a parameter to our posts url path, called endpoint. We did this primarily since in our Front-end, we would need to make to different types of calls, one to fetch posts like we did before, and a second to identify total number of pages for controlling our pagination buttons. Thus, one of the endpoints is 'posts' for fetching posts, and the other is 'pages'. Other than that, if there is any other endpoint requested, we will return a Status 404 Error.


Moving on, we have then created an instance of the Paginator class, and used it to separate our posts into pages of three. Now if our function receives a request for endpoint = "posts", then it will return the posts for the page number specifies, whereas if endpoint = "pages", then it will return the number of pages (which we will require for controlling our pagination buttons through JavaScript). Now let us get started with our buttons!



Fetching Number of Pages


We have now come to one of the most important parts of this tutorial, it's where we fetch the number of pages that we have in total, and we create our pagination (previous/next) buttons. What we will do is, make a call to our Posts View, and fetch the number of pages inside of a new JavaScript function. Then we will use this information to hide or show our buttons, and call our fetchPosts function when required:



function pagination () {
    fetch("/posts/pages")
    .then(response => response.json())
    .then(result => {
        if (result.pages > 1) {

            let counter = 1;
            let previous = document.createElement('li')
            previous.innerHTML = `<a class="page-link" href="#" aria-label="Next"><span aria-hidden="true">&laquo;</span></a>`
            previous.classList.add("page-item")
                
            let next = document.createElement('li')
            next.innerHTML = `<a class="page-link" href="#" aria-label="Next"><span aria-hidden="true">&laquo;</span></a>`
            next.classList.add("page-item")

            document.querySelector('#page-number').append(previous)
            document.querySelector('#page-number').append(next)

        }
    })
}


And just like that, we have added our pagination buttons to our page, under the condition that the number of pages fetched from our python view, should not be 1. I have added these elements to '#page-number' part of my index.html template. Another thing that we would want to do now is add eventListeners to our buttons, and call our fetchPost function:



function pagination () {
    fetch("/posts/pages")
    .then(response => response.json())
    .then(result => {
        if (result.pages > 1) {
                let counter = 1;
                let previous = document.createElement('li')
                previous.innerHTML = `<a class="page-link" href="#" aria-label="Next"><span aria-hidden="true">&laquo;</span></a>`
                previous.classList.add("page-item")
                
                let next = document.createElement('li')
                next.innerHTML = `<a class="page-link" href="#" aria-label="Next"><span aria-hidden="true">&laquo;</span></a>`
                next.classList.add("page-item")

                previous.addEventListener('click', function () {
                    counter--
                    fetch_post(counter)
                    if (counter === 1) {
                        previous.style.display = 'none'
                        next.style.display = 'block'
                    } else {
                        next.style.display = 'block'
                    }
                })

                next.addEventListener('click', function () {
                    counter++
                    fetch_post(counter)
                    if (counter >= result.pages) {
                        next.style.display = 'none'
                        previous.style.display = 'block'
                    } 
                        next.style.display = 'block'
                    
                })
                    previous.style.display = 'none'
                    document.querySelector('#page-number').append(previous)
                    document.querySelector('#page-number').append(next)

        }
    })
}


Wow, that is long. So, we are practically done with the major part of our pagination now. What we have just done is create a variable called counter (acting as our page number), which is initially 1 and is incremented every time the 'next' button is pressed, and decremented every time the 'previous' button is pressed. At the press of either of the buttons, we have also called our fetchPost function, which will display a fresh set of posts at each iteration by making a request to our Posts View with the appropriate page number (after we make some changes to it in the next step).


Another thing I have done here is control when each of the buttons should appear and disappear. If we think about it, what we want is for the previous button to appear on all pages but one, and for the next button to appear on all pages but the last. So that is exactly what I have done in the above function. This brings us another step closer to completing our Pagination function, so let's quickly throw in our finishing touches!



Completing Pagination


Almost there, now we just have some small ends to tie up and then we will have a perfectly functioning pagination feature at hand! Firstly, we need to reconfigure or update our fetchPost function to send requests to our Posts View with the page number as a parameter:



function fetch_post(counter) {
    document.querySelector('#main-section').innerHTML = ''
    fetch(`/posts/posts?page=counter`)
    .then(response => response.json())
    .then(posts => {
        posts.forEach(element => {
                let post = document.createElement('div')
                post.innerHTML = //post HTML code here!
               document.querySelector('#main-section').append(post)
                post.classList.add("box")
                post.id = element.id
        })
    }
}


Great, now once that is done, we need to actually call our pagination function in our 'DOMContentLoaded' EventListener as follows. We also want to clear out the innerHTML from our 'page-number' section of our HTML template, to avoid any repeated buttons from the previous iteration likes this:



document.addEventListener('DOMContentLoaded', function () {

	document.querySelector('#main-view').style.display = 'block';
	document.querySelector('#page-number').innerHTML = '';

	fetch_post(1)
	pagination()

})


Awesome job! With that we have implemented our Pagination feature successfully. If you see here, we have called our fetchPost function initially with a counter of 1, and simultaneously also called the pagination function and set our buttons. The result should now look something similar to this:



The next step would actually be to allow the user to jump to particular pages, by adding separate buttons. But that is a story for another day.



Conclusion

Wow, that was pretty exciting, no? You have successfully implemented back-end pagination in your Django project using JavaScript. You can now use this functionality in any of your Python projects, or you can even use the logic and structure in any other programming language you might be exploring. I would like to once again extend my heartfelt gratitude to all of you who made it through the entire post and I sincerely hope you learnt something beneficial. If you did so, don't forget to drop your likes and comments so I know that I should keep going! Before I sign off, always remember, Science (especially Computer Science) is all about experimentation, so never be afraid of trying out new ways of doing things.


Until Next Time ~


1,480 views0 comments
Post: Blog2_Post
bottom of page