🏷️ #django #python #backend #webdev

Django for Beginners #5 - Some Advanced Features

In this article, we’ll add some optional advanced features for our Django blog website, including a paginator, related posts, as well as a search feature.

➡️ Get the source code for FREE!

Create pagination in Django #

Paginator

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

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

Update the home view:

 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
26
27
28
29
def home(request):
    site = Site.objects.first()
    categories = Category.objects.all()
    tags = Tag.objects.all()
    featured_post = Post.objects.filter(is_featured=True).first()

    # Add Paginator
    page = request.GET.get("page", "")  # Get the current page number
    posts = Post.objects.all().filter(is_published=True)
    paginator = Paginator(posts, n)  # Showing n 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",
        {
            "site": site,
            "posts": posts,
            "categories": categories,
            "tags": tags,
            "featured_post":featured_post
        },
    )

Line 9 to 14, here you must consider three different conditions. If the page number is an integer, return the requested page; if the page number is not an integer, return page 1; if the page number is larger than the number of pages, return the last page.

Next, you need to put the paginator in the template, along with the list of posts like this:

templates/vendor/list.html

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<!-- Paginator -->
<nav
  class="isolate inline-flex -space-x-px rounded-md mx-auto my-5 max-h-10"
  aria-label="Pagination"
>
  {% if posts.has_previous %}
  <a
    href="?page={{ posts.previous_page_number }}"
    class="relative inline-flex items-center rounded-l-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20"
  >
    <span class="sr-only">Previous</span>
    <!-- Heroicon name: mini/chevron-left -->
    <svg
      class="h-5 w-5"
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 20 20"
      fill="currentColor"
      aria-hidden="true"
    >
      <path
        fill-rule="evenodd"
        d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z"
        clip-rule="evenodd"
      />
    </svg>
  </a>
  {% endif %}

  {% for i in posts.paginator.page_range %}
  {% if posts.number == i %}
  <a
    href="?page={{ i }}"
    aria-current="page"
    class="relative z-10 inline-flex items-center border border-blue-500 bg-blue-50 px-4 py-2 text-sm font-medium text-blue-600 focus:z-20"
    >{{ i }}</a
  >
  {% else %}
  <a
    href="?page={{ i }}"
    aria-current="page"
    class="relative inline-flex items-center border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20"
    >{{ i }}</a
  >
  {% endif %}
  {% endfor %}

  {% if posts.has_next %}
  <a
    href="?page={{ posts.next_page_number }}"
    class="relative inline-flex items-center rounded-r-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20"
  >
    <span class="sr-only">Next</span>
    <!-- Heroicon name: mini/chevron-right -->
    <svg
      class="h-5 w-5"
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 20 20"
      fill="currentColor"
      aria-hidden="true"
    >
      <path
        fill-rule="evenodd"
        d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
        clip-rule="evenodd"
      />
    </svg>
  </a>
  {% endif %}
</nav>

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
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def post(request, slug):
    site = Site.objects.first()
    requested_post = Post.objects.get(slug=slug)
    categories = Category.objects.all()
    tags = Tag.objects.all()

    # Related Posts
    ## Get all the tags related to this article
    post_tags = requested_post.tag.all()
    ## Filter all posts that contain tags which 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",
        {
            "site": site,
            "post": requested_post,
            "categories": categories,
            "tags": tags,
            "related_posts": related_posts,
        },
    )

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

Line 3, get the requested post using the slug.

Line 9, get all the tags that belongs to the requested post.

Line 11 to 16, 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. First, the current post will also be included in the query set, so we use 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 to 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 related posts, and finally move onto tag 3.

This 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
23
24
25
26
27
28
29
    <!-- Related posts -->

    <div class="grid grid-cols-3 gap-4 my-5">
      {% for post in related_posts %}
      <!-- post -->
      <div class="mb-4 ring-1 ring-slate-200 rounded-md h-fit hover:shadow-md">
        <a href="{% url 'post' post.slug %}"
          ><img
            class="rounded-t-md object-cover h-60 w-full"
            src="{{ post.featured_image.url }}"
            alt="..."
        /></a>
        <div class="m-4 grid gap-2">
          <div class="text-sm text-gray-500">
            {{ post.created_at|date:"F j, o" }}
          </div>
          <h2 class="text-lg font-bold">{{ post.title }}</h2>
          <p class="text-base">
            {{ post.content|striptags|truncatewords:30 }}
          </p>
          <a
            class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase text-sm font-semibold font-sans w-fit focus:ring"
            href="{% url 'post' post.slug %}"
            >Read more →</a
          >
        </div>
      </div>
      {% endfor %}
    </div>

Implement search in Django #

Next, you can add a search feature for your app. To create a search feature, you 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 to your sidebar:

templates/vendor/sidebar.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div class="col-span-1">
  <div class="border rounded-md mb-4">
    <div class="bg-slate-200 p-4">Search</div>
    <div class="p-4">
      <form action="{% url 'search' %}" method="POST" class="grid grid-cols-4 gap-2">
        {% csrf_token %}
        <input
          type="text"
          name="q"
          id="search"
          class="border rounded-md w-full focus:ring p-2 col-span-3"
          placeholder="Search something..."
        />
        <button
          type="submit"
          class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase font-semibold font-sans w-full focus:ring col-span-1"
        >
          Search
        </button>
      </form>
    </div>
  </div>
  . . .
</div>

Line 7-13, notice the name attribute of the input field, here we’ll call it q. The user input will be tied to the variable q and sent to the backend.

Line 5, when the button is clicked, the user will be routed to the URL with the name search, so you 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
18
19
20
21
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 #

templates/search.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{% extends 'layout.html' %}

{% block title %}
<title>Page Title</title>
{% endblock %}

{% block content %}
<div class="grid grid-cols-4 gap-4 py-10">
  <div class="col-span-3 grid grid-cols-1">

    {% include "vendor/list.html" %}

  </div>
  {% include "vendor/sidebar.html" %}
</div>
{% endblock %}

Now try to search something in the search form, and you should be returned only the posts you are requesting.

This brings the end to the Django tutorial for beginners, if you are interested, please check out my other tutorials as well:


If you think my articles are helpful, please consider making a donation to me. Your support is greatly appreciated.

Subscribe to my newsletter ➡️

✅ News and tutorials every other Monday

✅ Unsubscribe anytime

✅ No spam. Always free.