Pagination for the objects returned in 'else' clause

Viewed 25

I have following model, where pagination works ok only if I don't filter by tags. As soon as I filter ads by tags, pagination doesn't work.

class AdListView(ListView):
paginate_by = 2
model = Ad
context_object_name = 'ads'

def get_context_data(self, **kwargs):
    if self.request.GET.get('tags') is None:
        context = super().get_context_data(**kwargs)
        context.update(tags=Tag.objects.all())
        return context
    else:
        tag_submitted = Tag.objects.get(name=self.request.GET.get('tags'))
        ads_by_tag = tag_submitted.related_ads.all()
        context = {
            'ads': ads_by_tag
        }
        context.update(tags=Tag.objects.all())
        return context

What needs to be done so that pagination works for both cases, with and without filter?

below are models.py and template

models.py

class Tag(BaseModel):

def __str__(self):
    return str(self.name)


class Ad(BaseModel):
    name = models.CharField(max_length=100)
    description = models.TextField(max_length=500)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    seller = models.ForeignKey(Seller, on_delete=models.CASCADE)
    date_created = models.DateField(auto_now_add=True)
    date_modified = models.DateField(auto_now=True)
    tags = models.ManyToManyField(Tag, related_name='related_ads')
    price = models.PositiveIntegerField(default=0)
    archived = models.BooleanField(default=False)

    def __str__(self):
         return str(self.name)

    class Meta:
         ordering = ['-date_created']

template

{% block content %}
<div class="container">
    <div class="row">
        <div class="w-75">
            <table class="table">
                <thead>
                <tr>
                    <th scope="col">#</th>
                    <th scope="col">Title</th>
                </tr>
                </thead>
                <tbody>
                {% for ad in ads %}
                    <tr>
                        <td>{{ forloop.counter }}</td>
                        <td><a href="{% url 'ads-detail' ad.id %}"> {{ ad.name }} </a></td>
                    </tr>
                {% empty %}
                    <li>No ads yet.</li>
                {% endfor %}
                </tbody>
            </table>
        </div>
        <div class="w-25">
            <table class="table">
                <thead>
                <tr>
                    <th scope="col">Tags</th>
                </tr>
                </thead>
                <tbody>
                {% for tag in tags %}
                    <tr>
                        <td><a href="{% url 'ads-list'%}?tags={{ tag }}"> {{ tag }} </a></td>
                    </tr>
                {% empty %}
                    <li>No tags yet.</li>
                {% endfor %}
                </tbody>
            </table>
        </div>
    </div>
    <div class="row">
        <div class="pagination">
        <span class="step-links">
            {% if page_obj.has_previous %}
                <a href="?page=1">&laquo; first</a>
                <a href="?page={{ page_obj.previous_page_number }}">previous</a>
            {% endif %}

            <span class="current">
                Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
            </span>

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

so this is a project to learn django, so any other comments on how to improve the code also welcome.

1 Answers

You should not filter in the .get_context_data() [Django-doc]. In Django you pass the (possibly) filtered queryset in .get_queryset(…) [Django-doc]. Django will then paginate the queryset, and add it to the context. By overriding the item in the context, you now have removed the paginated queryset, and added a filtered queryset instead.

You thus provide the filtered queryset, and let Django do the proper pagination:

class AdListView(ListView):
    paginate_by = 2
    model = Ad
    context_object_name = 'ads'

    def get_search_querystring(self):
        copy = self.request.GET.copy()
        copy.pop(self.page_kwarg, None)
        return copy.urlencode()

    def get_queryset(self, *args, **kwargs):
        qs = super().get_queryset(*args, **kwargs)
        if 'tags' in self.request.GET:
            return qs.filter(tags__name=self.request.GET['tags'])
        return qs

    def get_context_data(*args, **kwargs):
        return super().get_context_data(*args, tags=Tag.objects.all(), **kwargs)

Another problem is the link in the template: by using ?page={{ page_obj.next_page_number }}, this will "drop" the query string [wiki] that contains the search query, and only use page as the only variable in the query string.

In the template we thus can use the URL as:

<a href="?page={{ page_obj.next_page_number }}&{{ view.get_search_queryset|safe }}">

the same should happen with the other pagination links.

Related