Django for Beginners #5

You can download the source code for this tutorial here.

We’ve covered everything we need to know to create a simple Django application. In this article, we’ll add some optional features for our website, including paginator, related posts, search function and user authentication with django-allauth. If you are not interested, just skip to the end of this article and start preparing for deployment.

Pagination in Django framework

pagination

When we add more and more posts to our blog, creating a paginator might be a good idea, since we don’t want to have too many posts on one page. To do that, we need to add some extra code to our views. Let’s take the home view as an example. First, we need to import some necessary packages:

1
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

Update home view:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def home(request):
    ...

    # Add Paginator
    page = request.GET.get('page', '') # Get the current page number
    posts = Post.objects.all().filter(is_published=True)
    paginator = Paginator(posts, 1) # Showing 1 post for every page

    try:
        posts = paginator.page(page)
    except PageNotAnInteger:
        posts = paginator.page(1)
    except EmptyPage:
        posts = paginator.page(paginator.num_pages)

    return render(request, 'home.html', {
        ...
        'posts': posts,
        ...
    })

Here I stripped away the unchanged codes to avoid confusion, but you need to remember to add them.

From line 9 to 14, here we need to consider three different conditions. If the page number is an integer, if the page number is not an integer, and if the page number is empty.

Next, we need to display the paginator in the template like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- Pagination -->
{% if posts.has_other_pages %}
<ul class="pagination">
    {% if posts.has_previous %}
    <li class="page-item"><a class="page-link" href="?page={{ posts.previous_page_number }}">&laquo;</a></li>
    {% else %}
    <li class="page-item disabled"><span class="page-link">&laquo;</span></li>
    {% endif %}
    {% for i in posts.paginator.page_range %}
    {% if posts.number == i %}
    <li class="page-item active"><span class="page-link">{{ i }}</span></li>
    {% else %}
    <li class="page-item"><a class="page-link" href="?page={{ i }}">{{ i }}</a></li>
    {% endif %}
    {% endfor %}
    {% if posts.has_next %}
    <li class="page-item"><a class="page-link" href="?page={{ posts.next_page_number }}">&raquo;</a></li>
    {% else %}
    <li class="page-item disabled"><span class="page-link">&raquo;</span></li>
    {% endif %}
</ul>
{% endif %}

Refresh the page and you should see the paginator at the bottom.

Paginator

You should do the same for all the pages that contain a list of posts.

related posts

The idea is to get the posts with the same tags.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def post(request, slug):
    requested_post = Post.objects.get(slug=slug)
    ...

    # Related Posts

    # Get all the tags related to this article
    post_tags = requested_post.tag.all()
    # Filter all posts that contain tags that are related to the current post, and exclude the current post
    related_posts_ids = Post.objects.all().filter(tag__in=post_tags).exclude(id=requested_post.id).values_list('id')
    related_posts = Post.objects.filter(pk__in=related_posts_ids)

    return render(request, 'post.html', {
        ...
        'related_posts': related_posts,
    })

This code is a little difficult to understand, but don’t worry, let’s analyze it line by line.

Line 2, get the requested post using the slug.

Line 8, get all the tags that are related to the requested post.

Line 10, this is where things get tricky. First, Post.objects.all() retrieves all posts from the database. And then, filter(tag__in=post_tags) retrieves all posts that have tags which are related to the current post. However, we have two problems. The current post will also be included in the query set, so we use the exclude(id=requested_post.id) to exclude the current post.

The second problem, however, is not that easy to understand. Let’s consider this scenario. Here we have three posts and three tags.

Tag ID Tag Name
1 Tag 1
2 Tag 2
3 Tag 3
Post ID Post Name
1 Post 1
2 Post 2
3 Post 3

And the posts and tags have a many-to-many relationship with each other.

Tag ID Post ID
1 2
1 3
1 1
2 1
2 2
2 3
3 2
Post ID Tag ID
1 1
1 2
2 1
2 2
2 3
3 1
3 2

Let’s say our current post is post 2, that means our related tags will be 1, 2 and 3. Now, when you are using filter(tag__in=post_tags), Django will first go to tag 1, find tag 1’s related posts, which is post 2, 3 and 1, and then go to tag 2, find tag 2’s posts, and finally move onto tag 3. That means filter(tag__in=post_tags) will eventually return [2,3,1,1,2,3,2]. After the exclude() method, it would return [3,1,1,3]. This is still not what we want, we need to find a way to get rid of the duplicates.

This is why we need to use values_list('id') to pass the post ids to the variable related_posts_ids and then use that variable to retrieve the related posts. This way will eliminate the duplicates.

Finally, we can display the related posts in the corresponding template:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- Related Posts -->
<h3>Related Posts</h3>
<div class="row">
    {% for post in related_posts %}
        <div class="col-md-4">
            <div class="card mb-4 box-shadow">
                <img class="card-img-top" src="{{ post.featured_image.url }}" alt="{{ post.title }}">
                <div class="card-body">
                    <h3 class="card-title">{{ post.title }}</h3>
                    <p class="card-text">{{ post.content|striptags|truncatewords_html:25 }}</p>
                    <div class="d-flex justify-content-between align-items-center">
                        <div class="btn-group">
                            <a href="{% url 'post' post.slug %}"
                               class="btn btn-sm btn-outline-secondary">Read
                                More →</a>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    {% endfor %}
</div>

Implementing the search function

Next, we can add a search function for our app. To create a search function, we need a search form in the frontend, which will send the search query to the view, and the view function will retrieve the qualified records from the database, and finally return a search page that will display the result.

The search form

First, let’s add a search form into our sidebar:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!-- Search widget-->
<div class="card mb-4">
    <div class="card-header">Search</div>
    <form class="card-body" action="{% url 'search' %}" method="POST" role="search">
        {% csrf_token %}
        <div class="input-group">
            <input class="form-control" type="text" placeholder="Enter search term..."
                aria-label="Enter search term..." aria-describedby="button-search" name="q" />
            <button class="btn btn-primary" id="button-search" type="submit">Go!</button>
        </div>
    </form>
</div>

Line 7-8, adds name attribute to the input field, its value could be anything, here we’ll call it q.

When the button is clicked, it’ll go to the URL with the name 'search', so we need to register the corresponding URL pattern.

1
path('search', views.search, name='search'),

The search view

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def search(request):
    site = Site.objects.first()
    categories = Category.objects.all()
    tags = Tag.objects.all()

    query = request.POST.get('q', '')
    if query:
        posts = Post.objects.filter(is_published=True).filter(title__icontains=query)
    else:
        posts = []
    return render(request, 'search.html', {
        'site': site,
        'categories': categories,
        'tags': tags,
        'posts': posts,
        'query': query,
    })

The search template

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{% extends 'layout.html' %}

{% block meta %}
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <meta name="description" content="{{ site.description }}" />
    <meta name="author" content="Eric Hu" />
    <title>{{ query }} - {{ site.name }}</title>
{% endblock meta %}

{% block header %}
    <!-- Page header with logo and tagline -->
    <header class="py-5 bg-light border-bottom mb-4">
        <div class="container">
            <div class="text-center my-5">
                <h1 class="fw-bolder">Search Query: {{ query }}!</h1>
            </div>
        </div>
    </header>
{% endblock header %}

{% block content %}
    {% include 'vendor/post-list.html' %}
    {% include 'vendor/sidebar.html' %}
{% endblock content %} 

User Authentication

User authentication is a very important part in web development, and it is not as easy as it seems. You have to consider different scenarios, for example, what if the user forgets the password, what if the user lost the email account, and how do you verify the user’s information, etc.

With Django, there are two different ways we can build a user authentication system. This first one is by using Django’s built-in auth module. This method is the standard, but it is relatively complicated. You have to create the views and templates on your own. I’ll create another tutorial on this subject, for now you can use this article for reference if you are interested in this method.

The second method is by using the django-allauth package. Setup the package as instructed in this article. Go to http://127.0.0.1:8000/accounts/login/, and you should see the sign in page.

sign in page

Preparing for deployment

Finally, it’s time to deploy our Django project. Instead of doing everything manually, we use a server management panel, which is much more beginner-friendly. There are a few things we need to do before deploying our website. First, we need to prepare the static and media files. Make sure you have the following code in your settings.py file.

1
2
3
4
5
STATIC_ROOT =  os.path.join(BASE_DIR, 'static')
STATIC_URL = '/static/'

MEDIA_ROOT =  os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

Run the following command:

1
python manage.py collectstatic

This command will collect all static files from all the apps you created into the static folder.

The media files, on the other hand, Django discourages serving media files directly from the server in the production environment. You need to configure your server settings. Take Apache as an example:

1
2
3
4
5
6
Alias /media/ /path/to/media/

<Directory /path/to/media/>
Order deny,allow
Allow from all
</Directory>

Notice that /media/ matches the setting for MEDIA_URL, and /path/to/media/ is the absolute path to the media/ folder.

Run Deployment Check

Before we deploy our project, we need to check a few things. Go to terminal and run manage.py check --deploy. This command will check the SECRET_KEY, DEBUG, ALLOWED_HOSTS and a few other sensitive settings for you.

Please check your own settings against this documentation.

Deploy Our Django Project

First, let’s set up a new VPS. I assume you already bought one from a cloud infrastructure provider such as DigitalOcean, Vultr or Linode. Now, you have two choices, you can either follow this tutorial and use a control panel to deploy our project, or you can do it manually.

The control panel method does not guarantee success, I choose to use it here because it is a lot easier for beginners. So if it does not work for you, please refer to this article and deploy it manually. There is a lot of different panels you can choose from. Here I’ll use aaPanel as an example. (aaPanel Demo)

Run the shellcode on the official site of aaPanel. Make sure you are running CentOS on your VPS. aaPanel is developed on CentOS, using other systems may cause errors. After the installation process is finished. Follow the instructions on the screen and log into the panel.

aapanel

From now on, everything should be straightforward. Just install the latest version of the Apache server and Python. You can install MySQL if you want. It is more robust and secure. But if you don’t, Django will use SQLite by default. You can also install other tools, but for now, these are all we need.

We can use the Python Manager tool to deploy our Django project.

Python project manager

I tested this tool on my own server and it worked for me. However, since it is still under development, there is a chance that it won’t work for you. In that case, please refer to this post on how to deploy Django project manually.

First, go to “Files”, and create a new directory.

new directory

After that, upload your entire project to the folder you just created.

upload project

Next, open the Python Manager and add a new project.

Add a project with python manager

“Path” should point to the dir that contains manage.py. “Version” is the version of Python that you installed.

The startup module is can be either gunicorn or uwsgi. They are both interfaces that allow Django and the webserver to communicate. gunicorn did not work for me, so maybe you should try uwsgi first.

The startup dir should point to the folder that contains wsgi.py. And finally, the “Port” can be anything you want. Once you click confirm, it will take a few minutes to install all dependencies.

Finally, map the domain to the server and we are good to go!.


comments powered by Disqus