Django DetailView pagination, no numbers to navigate

I have a Django forum app, its structure is “forum → subforum → topic → comments”. Comments are displayed on their related topic’s page, so a topic’s view handles Topic together with its comments. Topic itself includes its subject and the initial comment (first_comment). I purposefully use DetailView here instead of ListView, as 1) I want to slve the problem this way; 2) I’ve tried to change it to ListView, but it didn’t help.

I’ve looked through some tutorials and earlier questions, so somehow I’ve made my pagination work in the sense that it cuts the amount of displayed comments by the number in the parameter in Paginator (in the example it’s 5). But, unfortunately, I cannot reproduce any links, numbers of pages to navigate through the pagination. And I don’t even know, where is the cause of this problem hidden - either in html code, either in the view’s code.

Asking for help and advice of the community.

View:

class ShowTopic(DetailView):
    model = Topic
    template_name = "forum/topic.html"
    slug_url_kwarg = 'topic_slug'
    context_object_name = 'topic'

    def get_context_data(self, **kwargs):
        topic = get_object_or_404(Topic, slug=self.kwargs['topic_slug'])
        comments = self.get_comments(topic)
        comments_number = len(Comment.objects.filter(topic__id=topic.id))
        context = {'menu': menu,
                   'topic': topic,
                   #'comments': comments,
                   'page_obj': comments,
                   'comm_num': comments_number}
        return context

    def get_comments(self, topic):
        qs = Comment.objects.filter(topic=topic)
        paginator = Paginator(qs, 5)
        page = self.request.GET.get('page')
        comments = paginator.get_page(page)
        return comments

topic.html (the template of a topic and its comments)

{% extends 'base.html' %}
{% load static %}

{% block content %}
<h1>{{ topic.subject }}</h1>

<div class="container-posts">
  <div class="row">
    <div class="card-subtitle">Created by: {{ topic.creator }}, time: {{ topic.created }}</div>
    <div class="card-subtitle">Number of comments in the topic: {{ comm_num }}</div>
    <div class="container-posts">
        <a href="add_comment"><button>Leave a comment</button></a>
    </div>
    <div class="card-body">
      <div class="media">
        {% if p.photo %}
          <img src="{{p.photo.url}}" alt="" width="300" height="300">
        {% else %}
          <img src="{% static 'core/images/Swr-portrait-aya.png' %}" alt="" width="150" height="150">
        {% endif %}
      </div>
      <h5 class="card-subtitle">{{ topic.creator }}</h5>
      <h5 class="card-subtitle">Created: {{topic.created|date:"d.m.Y H:i"}}</h5>
      {% autoescape off %}
      <p class="card-text">{{topic.first_comment|linebreaks|truncatewords:200}}</p>
      {% endautoescape %}
      <a href="{% url 'forum:edit_comment' topic.subforum.slug topic.slug topic.id %}">Edit</a>
      <div class="clear"></div>
      <hr>
    </div>

    <!--Comments section -->
    {% for p in page_obj %}
      <div class="card-posts">
        <div class="card-body">
          <div class="media">
            {% if p.photo %}
              <img src="{{p.photo.url}}" alt="" width="300" height="300">
            {% else %}
              <img src="{% static 'core/images/Swr-portrait-aya.png' %}" alt="" width="150" height="150">
            {% endif %}
          </div>
          <h5 class="card-subtitle">{{ p.author }}</h5>
          <h5 class="card-subtitle">Created: {{p.created|date:"d.m.Y H:i"}}</h5>
          {% autoescape off %}
          <p class="card-text">{{p.content|linebreaks|truncatewords:200}}</p>
          {% endautoescape %}
          <div class="card-subtitle">
            <a href="{% url 'forum:edit_comment' topic.subforum.slug topic.slug p.id %}">Edit</a>
            <a href="{% url 'forum:delete_comment' topic.subforum.slug topic.slug p.id %}">Delete</a></div>
          <div class="clear"></div>
          <hr>
        </div>
      </div>
    {% endfor %}

  </div>
</div>

{% endblock %}

{% if is_paginated %}
    {% if page_obj.has_other_pages %}
        <div class="pagination">
            <ul>
                {% if page_obj.has_previous %}
                    <li><a href="?page=1">&laquo; first</a></li>
                    <li><a href="?page={{ page_obj.previous_page_number }}">&lt;</a></li>
                {% endif %}
            </ul>

            <ul>
                {% for p in paginator.page_range %}
                    {% if page_obj.number == p %}
                        <li class="page-num page-num-selected">{{ p }}</li>
                    {% elif page_obj.number|add:-1 and page_obj.number|add:1 %}
                        <li class="page-num">
                            <a href="?page={{ p }}">{{ p }}</a>
                        </li>
                    {% endif %}
                {% endfor %}
            </ul>
            <ul>
                {% if page_obj.has_next %}
                    <li><a href="?page={{ page_obj.next_page_number }}">&gt;</a></li>
                    <li><a href="?page={{ page_obj.paginator.num_pages }}">last &raquo;</a></li>
                {% endif %}
            </ul>
        </div>
    {% endif %}
{% endif %}

urls.py:

from django.urls import path

from forum.views import *


app_name = 'forum'

urlpatterns = [
    path('', SubForumListView.as_view(), name='forum'),
    path('<slug:subforum_slug>/', TopicListView.as_view(), name='subforum'),
    path('<slug:subforum_slug>/add_topic/', AddTopic.as_view(), name="add_topic"),
    path('<slug:subforum_slug>/topics/<slug:topic_slug>/', ShowTopic.as_view(), name='topic'),
    path('<slug:subforum_slug>/topics/<slug:topic_slug>/add_comment/', AddComment.as_view(), name="add_comment"),
    path('<slug:subforum_slug>/topics/<slug:topic_slug>/<int:pk>/edit_comment/', UpdateComment.as_view(), name="edit_comment"),
    path('<slug:subforum_slug>/topics/<slug:topic_slug>/<int:pk>/delete_comment/', DeleteComment.as_view(), name="delete_comment"),
]

Topic and Comment models:

class Topic(models.Model):
    subject = models.CharField(verbose_name='Заголовок', max_length=255, unique=True)
    first_comment = models.TextField(verbose_name='Сообщение', max_length=2000, default='')
    slug = models.SlugField(default='', unique=True, max_length=25, editable=False)
    subforum = models.ForeignKey('Subforum',
                                 verbose_name='Раздел',
                                 on_delete=models.CASCADE,
                                 related_name='subforum')
    creator = models.ForeignKey(User,
                                verbose_name='Создатель темы',
                                on_delete=models.SET('deleted'),
                                related_name='creator')
    created = models.DateTimeField(auto_now_add=True)
    closed = models.BooleanField(default=False)
    objects = models.Manager()

    class Meta:
        ordering = ['id']
        verbose_name = 'Обсуждения'
        verbose_name_plural = 'Обсуждения'

    def __str__(self):
        return self.subject

    def save(self, *args, **kwargs):
        if not self.id:
            self.slug = f'topic-{slugify(self.subject)}'
            return super(Topic, self).save(*args, **kwargs)

    def get_absolute_url(self):
        return reverse('forum:topic', kwargs={'topic_slug': self.slug, 'subforum_slug': self.subforum.slug})


class Comment(models.Model):
    topic = models.ForeignKey('Topic',
                              verbose_name='Тема',
                              on_delete=models.CASCADE,
                              related_name='comments')
    author = models.ForeignKey(User,
                               verbose_name='Комментатор',
                               on_delete=models.SET('deleted'),
                               related_name='author')
    content = models.TextField(verbose_name='Текст', max_length=2000)
    created = models.DateTimeField(verbose_name='Дата публикации', auto_now_add=True)
    updated = models.DateTimeField(verbose_name='Дата изменения', auto_now=True)
    objects = models.Manager()

    class Meta:
        ordering = ['created']
        verbose_name = 'Комментарии'
        verbose_name_plural = 'Комментарии'

    def __str__(self):
        return f'Post of {self.topic.subject} is posted by {self.author.username}.'

I’ve also tried to convert it to ListView, but it doesn’t work as well, now it doesn’t even restrict the amount of comments displayed on the page:

class ShowTopic(ListView):
    model = Topic
    paginate_by = 5
    template_name = "forum/topic.html"
    slug_url_kwarg = 'topic_slug'
    context_object_name = 'topic'

    def get_context_data(self, **kwargs):
        topic = get_object_or_404(Topic, slug=self.kwargs['topic_slug'])
        comments = Comment.objects.filter(topic=topic)
        comments_number = len(Comment.objects.filter(topic__id=topic.id))
        context = {'menu': menu,
                   'topic': topic,
                   #'comments': comments,
                   'page_obj': comments,
                   'comm_num': comments_number}
        return context

You are not calling super().get_context_data(), which means you’re losing DetailView’s default context.

Try this in your views.py:

class ShowTopic(DetailView):
    model = Topic
    template_name = "forum/topic.html"
    slug_url_kwarg = 'topic_slug'
    context_object_name = 'topic'
    paginate_by = 5

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        topic = self.get_object()
        
        # Get comments and set up pagination
        comments_list = Comment.objects.filter(topic=topic).order_by('created')
        paginator = Paginator(comments_list, self.paginate_by)
        page_number = self.request.GET.get('page', 1)
        page_obj = paginator.get_page(page_number)
        
        # Add pagination context
        context.update({
            'menu': menu,  # Assuming menu is defined elsewhere
            'comments': page_obj,
            'page_obj': page_obj,
            'is_paginated': page_obj.has_other_pages(),
            'paginator': paginator,
            'comm_num': comments_list.count(),
        })
        
        return context

Now your topic.html should be something like this:

{% extends 'base.html' %}
{% load static %}

{% block content %}
<!-- Your existing topic display code remains the same until the comments section -->

<!-- Comments section -->
{% for p in page_obj %}
    <!-- Your existing comment display code -->
{% endfor %}

{# Pagination section - place this INSIDE your content block #}
{% if page_obj.has_other_pages %}
    <div class="pagination">
        {% if page_obj.has_previous %}
            <a href="?page=1">&laquo; first</a>
            <a href="?page={{ page_obj.previous_page_number }}">&lsaquo; previous</a>
        {% endif %}

        {% for num in page_obj.paginator.page_range %}
            {% if page_obj.number == num %}
                <span class="current">{{ num }}</span>
            {% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
                <a href="?page={{ num }}">{{ num }}</a>
            {% endif %}
        {% endfor %}

        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}">next &rsaquo;</a>
            <a href="?page={{ page_obj.paginator.num_pages }}">last &raquo;</a>
        {% endif %}
    </div>
{% endif %}

{% endblock %}

Test it and tell me how it goes.

1 Like

Thank you very much!
It works, though it is somehow doubled:

« first ‹ previous 1 2 3 next › last »
« first
<
1
2
3
>
last »

Also, I’ve found that it’s better to create a special block in ‘base.html’ (like {% block pagination %}) and to override it where it needs to be.
Thank you very much for your help!
Update: I’ve commented the previous code in the template via <!-- -->, but it somehow didn’t want to be commented, so worked along with the new one. I’ve deleted it, and the additional navigation was erased.

No problem! Keep going!

1 Like