Django, django-filter and pagination

4.3k Views Asked by At

my goal is to have a 'user_profile' page that displays relevant information of the user of interest.

Furthermore, the 'user_profile' page should include all the posts that were created by the respective user as new blog entries.

These posts, however, should be filterable with the application 'django-filter' and be paginated. At the moment I have difficulties to paginate the filtered posts. So my question is how to achieve the latter?

So far, I used following approach:

filters.py

import django_filters

class AccountPostFilter(django_filters.FilterSet):
title = django_filters.CharFilter(lookup_expr='icontains')
category = django_filters.ChoiceFilter(choices=cat_list)

class Meta:
    model = Post
    fields = ['title', 'category']

views.py

class UserProfile(DetailView, MultipleObjectMixin):
model = Account
template_name = 'account/user_profile.html'
paginate_by = 5


def get_context_data(self, **kwargs):
    posts = Post.objects.all().filter(author=self.kwargs['pk'])
    context = super().get_context_data(object_list=posts, **kwargs)
    context['filterset'] = AccountPostFilter(self.request.GET, queryset=posts)

    return context

Thank you very much for your time. Best wishes, Daniel

2

There are 2 best solutions below

0
On

There is another way of doing this, and do it in a clean and professional way, which will save you the trouble of using Django Filters:

Create a helper function called clean_filters (This will help you clean filters that come in from the brownser:

def clean_filters(filters):
    filters = {k: v for k, v in filters.items() if v}
    return filters

Create another help function called search (this will help you get the parameters from the GET request and put them in a **filters inside the django filter directive. And return them back with the paginator so you can keep the same filters when moving from page to page):

from 'your_utils_file' import clean_filters
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

def search(request):
    filters = {
        "account__first_name__icontains": request.GET.get("fname_kw"), # put your filters here
        "account__last_name__icontains": request.GET.get("lname_kw"), # put your filters here
    }

    html_queries = {
        "fname_kw": request.GET.get("fname_kw"),
        "lname_kw": request.GET.get("lname_kw"),
    }

    filters = clean_filters(filters)
    html_queries = clean_filters(html_queries)

    posts = Post.objects.filter(**filters) # put your model here

    page = request.GET.get('page', 1)
    paginator = Paginator(posts, 8)
    try:
        posts= paginator.page(page)
    except PageNotAnInteger:
        posts= paginator.page(1)
    except EmptyPage:
        posts= paginator.page(paginator.num_pages)

    return posts

Here is your view (this simply calls the search function to reduce the code of your view and make it easy for your code to maintain):

def search_page(request):
    posts = search(request)

    if posts is not None:
        context = {
            'posts': posts,
        }
        return render(request, "core/index.html", context)
    return redirect("index")

Here is your HTML (just a classic pagination code for Django and Bootstrap. This also has the filter and the value of the filter in a loop inside the GET request):

<div class="mb-5">
{% if posts.has_other_pages %}
    <nav aria-label="Page navigation example">
        <ul class="pagination justify-content-center">
            {% if posts.has_previous %}
            <li class="page-item">
                <a class="page-link" href="?page={{ posts.previous_page_number }}{% for fil, fil_value in filters.items %}&{{fil}}={{fil_value}}{% endfor %}" tabindex="-1">
                    <i class="fa fa-angle-left"></i>
                    <span class="sr-only">Prev</span>
                </a>
            </li>
            {% else %}
            <li class="page-item disabled">
                <a class="page-link" href="javascript:void(0)" tabindex="-1">
                    <i class="fa fa-angle-left"></i>
                    <span class="sr-only">Prev</span>
                </a>
            </li>
            {% endif %}
            {% for i in posts.paginator.page_range %}
            {% if posts.number == i %}
                <li class="page-item active"><a class="page-link" href="javascript:void(0)">{{ i }}</a></li>
            {% else %}
                <li class="page-item"><a class="page-link" href="?page={{ i }}{% for fil, fil_value in filters.items %}&{{fil}}={{fil_value}}{% endfor %}">{{ i }}</a></li>
            {% endif %}
            {% endfor %}
            {% if posts.has_next %}
            <li class="page-item">
                <a class="page-link" href="?page={{ posts.next_page_number }}{% for fil, fil_value in filters.items %}&{{fil}}={{fil_value}}{% endfor %}">
                    <i class="fa fa-angle-right"></i>
                    <span class="sr-only">Next</span>
                </a>
            </li>
            {% else %}
            <li class="page-item disabled">
                <a class="page-link" href="javascript:void(0)">
                    <i class="fa fa-angle-right"></i>
                    <span class="sr-only">Next</span>
                </a>
            </li>
            {% endif %}  
        </ul>
    </nav>
{% endif %}
0
On

An old question but, maybe there are people who want to achieve this in a basic way like me.

But I do not promise It will be perfect. In my case this paginated page was staff only and we didn't give too much attention. But it works. I solved it by using js.

Classic filterset and pagination view approach:

# myview.py

# will be used to apply filters on UI.
filterset = MyFilterSet(
    request.GET, queryset=MyModel.objects.all()
)

# divided by 20 object per page.
paginator = Paginator(filterset.qs, 20)

page = request.GET.get('page')

# will be used to apply pagination and iteration of objects.
try:
    paginated_objects = paginator.page(page)
except PageNotAnInteger:
    paginated_objects = paginator.page(1)
except EmptyPage:
    paginated_objects = paginator.page(paginator.num_pages)

context = {
    "filter": filterset,
    "paginated_objects": paginated_objects
}

Filter form:

<!-- mytemplate.html FILTER FORM -->

<form method="get" id="djangoFilterSetForm">
    <div class="row">
        {% for filter_form in filter.form %}
            <div class="col">
                <label>{{ filter_form.label }}</label>
                {{ filter_form }}
            </div>
        {% endfor %}
    </div>
    {% if show_submit_button %}
        <button type="submit">
            Submit Filter
        </button>
    {% endif %}
    <a href="{{ request.path }}">
        Reset Filters
    </a>
</form>

Pagination:

<!-- mytemplate.html PAGINATION -->

<div id="pagination_container">
    <span class="page-links">
        {% if paginated_objects.has_previous %}
            <button type="button" onclick="navigateUrl('{{ request.get_full_path }}', {{ paginated_objects.previous_page_number }})">Previous</button>
        {% endif %}
        <span class="page-current">
            {{ paginated_objects.number }} / {{ paginated_objects.paginator.num_pages }}
        </span>
        {% if paginated_objects.has_next %}
            <button type="button" onclick="navigateUrl('{{ request.get_full_path }}', {{ paginated_objects.next_page_number }})">Previous</button>
        {% endif %}
    </span>
</div>

Iteration of objects:

<!-- mytemplate.html QUERYSET ITERATION -->
{% for my_object in paginated_objects %}
    <!-- ... doing something with my_object ... -->
{% endfor %}

JS Solution:

<!-- mytemplate.html JS SOLUTION -->

<script type="text/javascript">

    function arrangePageQuery(urlPath, pageToNavigate) {
        if (window.location.href.includes("?" )) {
            // there's filter queries in url:
            window.location.href = urlPath + "&page=" + pageToNavigate;
        } else {
            // there's no filter queries:
            window.location.href = urlPath + "?page=" + pageToNavigate;
        }
    }

    function navigateUrl(fullPath, pageToNavigate) {
        
        if (window.location.href.includes("?page=")) {
            // to avoid appending when there is no filter queries, remove page query:
            let baseFullPath = window.location.href.split("?page=")[0]
            window.location.href = baseFullPath + "?page=" + pageToNavigate;
            return
        }

        if (window.location.href.includes("&page=")) {
            // to avoid appending when there is filter queries, remove page query
            let baseFullPath = window.location.href.split("&page=")[0]
            arrangePageQuery(baseFullPath, pageToNavigate);
        } else {
            arrangePageQuery(fullPath, pageToNavigate)
        }
    }
</script>