Django for Beginners #5 - Some Advanced Features
Tags: django, python, webdev, beginners
Published: 2023-03-28 14:40:00

> [Download source code here.](https://www.ericsdevblog.com/posts/django-for-beginners-1?ref=Dev_Community/) ⬅️

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/di4zyk7xai8jbm8bpik5.png)

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:

```python
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
```

<script async data-uid="cb570fac5e" src="https://ericsdevblog.ck.page/cb570fac5e/index.js"></script>

Update the `home` view:

```python
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`

```django
<!-- 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.

## Find related posts in Django

![related posts](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/54ytppxfsiwzgbryp0cs.png)

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

```python
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 `id`s 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:

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

```django
<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.

```python
path('search', views.search, name='search'),
```

### The search view

```python
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`

```django
{% 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:

- [Laravel for Beginners](https://www.ericsdevblog.com//posts/laravel-for-beginners-1?ref=Dev_Community/)
- [Vue.js for Beginners](https://www.ericsdevblog.com//posts/vuejs-for-beginners-1?ref=Dev_Community/)
- [Create a Modern Application with Django and Vue](https://www.ericsdevblog.com//posts/create-a-modern-application-with-django-and-vue-1?ref=Dev_Community/)
- [Beginner’s Roadmap to Web Development](https://www.ericsdevblog.com/series/beginners-roadmap-to-web-development?ref=Dev_Community/)