How to paginate Django with other get variables?

Viewed 34698

I am having problems using pagination in Django. Take the URL below as an example:

http://127.0.0.1:8000/users/?sort=first_name

On this page I sort a list of users by their first_name. Without a sort GET variable it defaults to sort by id.

Now if I click the next link I expect the following URL:

http://127.0.0.1:8000/users/?sort=first_name&page=2

Instead I lose all get variables and end up with

http://127.0.0.1:8000/users/?page=2

This is a problem because the second page is sorted by id instead of first_name.

If I use request.get_full_path I will eventually end up with an ugly URL:

http://127.0.0.1:8000/users/?sort=first_name&page=2&page=3&page=4

What is the solution? Is there a way to access the GET variables on the template and replace the value for the page?

I am using pagination as described in Django's documentation and my preference is to keep using it. The template code I am using is similar to this:

{% if contacts.has_next %}
    <a href="?page={{ contacts.next_page_number }}">next</a>
{% endif %}
21 Answers

After some playing around I found a solution... although I don't know if it's really a good one. I'd prefer a more elegant solution.

Anyway I pass the request to the template and am able to access all the GET variables via request.GET. Then I loop through the GET dictionary and as long as the variable isn't page I print it.

{% if contacts.has_previous %}
    <a href="?page={{ contacts.previous_page_number }}{% for key,value in request.GET.items %}{% ifnotequal key 'page' %}&{{ key }}={{ value }}{% endifnotequal %}{% endfor %}">previous</a>
{% endif %}

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

{# I have all of this in one line in my code (like in the previous section), but I'm putting spaces here for readability.  #}
{% if contacts.has_next %}
    <a href="?page={{ contacts.next_page_number }}
        {% for key,value in request.GET.items %}
            {% ifnotequal key 'page' %}
                &{{ key }}={{ value }}
            {% endifnotequal %}
        {% endfor %}
    ">next</a>
{% endif %}

In your views.py you will somehow access the criteria on which you sort, e.g. first_name. You'll need to pass that value to the template and insert it there to remember it.

Example:

{% if contacts.has_next %}
    <a href="?sort={{ criteria }}&page={{ contacts.next_page_number }}">next</a>
{% endif %}

@skoval00 's answer is the most elegant, however it adds duplicate &page= query parameters to the url.

Here is the fix:

from urllib.parse import urlencode
from django import template

register = template.Library()

@register.simple_tag(takes_context=True)
def url_replace(context, next_page):
    query = context['request'].GET.copy().urlencode()
    
    if '&page=' in query:
        url = query.rpartition('&page=')[0]
    else:
        url = query
    return f'{url}&page={next_page}'

My solution is based on this one above with the slight improvement to remove &page= from appearing multiple times. See this comment

    @register.simple_tag(takes_context=True)
    def url_replace(context, **kwargs):
        query = context['request'].GET.copy()
        query.pop('page', None)
        query.update(kwargs)
        return query.urlencode()

This line query.pop('page', None) silently removes the page from the url

Another slight modification to skoval00 and Reinstate Monica to fully get rid of duplication and avoid the ugly ?&page=1 part:

from urllib.parse import urlencode
from django import template

register = template.Library()

@register.simple_tag(takes_context=True)
def url_replace(context, next_page):
    if query.startswith('page') or not len(query):
        new_url = f'page={next_page}'
    elif '&page=' in query:
        get_params = query.rpartition('&page=')[0] # equivalent to .split('page='), except more efficient 
        new_url = f'{get_params}&page={next_page}'
    else:
        new_url = f'{query}&page={next_page}'
    return new_url

@Elrond Supports Monica

@register.simple_tag(takes_context=True)
def url_replace(context, **kwargs):
    query = context['request'].GET.copy()
    for key in kwargs:
        query[key] = kwargs[key]
    return query.urlencode()

Use in template

<a class="page-link" href="?{% url_replace p=1 q='bar'%}">

Every such link you put in your view has to be equipped with relevant parameters. There is no implicit magic that would convert:

http://127.0.0.1:8000/users/?page=2

into:

http://127.0.0.1:8000/users/?sort=first_name&page=2

So what you need is some Sorter object/class/function/snippet (whatever might fit here without overdoing it), that would act similarly to django.core.paginator.Paginator, but would handle sort GET parameter.

It could be as simple as this:

sort_order = request.GET.get('sort', 'default-criteria')

<paginate, sort>

return render_to_response('view.html', {
    'paginated_contacts': paginated_contacts,  # Paginator stuff
    'sort_order': sort_order if sort_oder != 'default-criteria' else ''
})

Then, in your view:

{% if contacts.has_next %}
    <a href="?page={{ contacts.next_page_number }}{%if sort_order%}&sort={{sort_oder}}{%endif%}">next</a>
{% endif %}

I could be made more generic, but I hope you get the concept.

your code should be like:

{% if contacts.has_next %}
<a href="?page={{ contacts.next_page_number }}{% for key,value in request.GET.items %}{% ifnotequal key 'page' %}&{{ key }}={{ value }}{% endifnotequal %}{% endfor %}">next</a>
{% endif %}

this works for me and i find it simpler

from django.http import HttpRequest


def get_query_params_href(req: HttpRequest):
    query_strings = req.GET.dict()
    string = '?'
    for i in query_strings:
        string += f'{i}={query_strings[i]}&'
return string[0:string.__len__()-1]
Related