DEV Community

Cover image for Django for Beginners #5 - Some Advanced Features
Eric Hu
Eric Hu

Posted on • Edited on • Originally published at ericsdevblog.com

Django for Beginners #5 - Some Advanced Features

Download source code here. ⬅️

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.

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:

from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
Enter fullscreen mode Exit fullscreen mode

Update the home view:

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
        },
    )
Enter fullscreen mode Exit fullscreen mode

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

<!-- 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>

Enter fullscreen mode Exit fullscreen mode

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

Find related posts in Django

related posts

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

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,
        },
    )
Enter fullscreen mode Exit fullscreen mode

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:

    <!-- 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>
Enter fullscreen mode Exit fullscreen mode

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

<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>
Enter fullscreen mode Exit fullscreen mode

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.

path('search', views.search, name='search'),
Enter fullscreen mode Exit fullscreen mode

The search view

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,
        },
    )
Enter fullscreen mode Exit fullscreen mode

The search template

templates/search.html

{% 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 %}
Enter fullscreen mode Exit fullscreen mode

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:

Top comments (0)