88

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 %}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
vagabond
  • 1,717
  • 4
  • 19
  • 21

21 Answers21

73

I thought the custom tags proposed were too complex, this is what I did in the template:

<a href="?{% url_replace request 'page' paginator.next_page_number %}">

And the tag function:

@register.simple_tag
def url_replace(request, field, value):

    dict_ = request.GET.copy()

    dict_[field] = value

    return dict_.urlencode()

If the url_param is not yet in the url, it will be added with value. If it is already there, it will be replaced by the new value. This is a simple solution the suits me, but does not work when the url has multiple parameters with the same name.

You also need the RequestContext request instance to be provided to your template from your view. More info here:

http://lincolnloop.com/blog/2008/may/10/getting-requestcontext-your-templates/

mpaf
  • 6,597
  • 6
  • 38
  • 42
  • 6
    You can even refactor to not need to pass `request` as a parameter. See this answer http://stackoverflow.com/a/2160298/1272513 – alfetopito Jun 24 '15 at 14:18
49

I think url_replace solution may be rewritten more elegantly as

from urllib.parse import urlencode
from django import template

register = template.Library()

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

with template string simplified to

<a href="?{% url_replace page=paginator.next_page_number %}">
skoval00
  • 629
  • 5
  • 7
  • 4
    Thanks, this works! For Python 3, use `urllib.parse.urlencode()`. See [this question](http://stackoverflow.com/a/28906913/4323648). – arogachev Aug 09 '16 at 07:29
  • And for Python 2.7, it would be `import urllib` and `return urllib.urlencode(query)`. – S_M Jun 14 '17 at 05:25
  • For this tag to work `django.template.context_processors.request` context processor should be enabled. Though it is enabled by default. – skoval00 Jun 17 '17 at 09:28
  • 1
    For GET params with multiple values for the same key it's better to use: `query = context['request'].GET.copy()` and `return query.urlencode()` – jakubste Mar 29 '19 at 13:05
  • 9
    cons - it creates duplicate parameter in url like this: `&p=2&p=3&p=4` – Shaig Khaligli Apr 04 '19 at 10:03
  • @ShaigKhaligli I fixed that issue in [my answer](https://stackoverflow.com/a/57899037/10746224) – Lord Elrond Sep 12 '19 at 02:46
  • 3
    To remove the multiple page=&page values. Simply add: if query.get('page'): query.pop('page') – themissionmars Dec 04 '20 at 12:54
14

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 %}
vagabond
  • 1,717
  • 4
  • 19
  • 21
  • 2
    This approach works, but has a few flaws: 1. It violates DRY principle - you're repeating your code, which means that if you want to change something in it, you have to change it in all places you copied it to. 2. It slightely violates Model-View-Controller (or Model-Template-View, as Django creator call it) design pattern - Templates should be used to just render data. 3. It causes rendundant/meaningless GET parameters to be passed around all the time - thit isn't probably a big problem, but in my opinion it's more elegant to filter out such parameters. – Tomasz Zieliński Jan 12 '10 at 09:26
  • 2
    Supplement to previous comment: If you insist on handling this in template, then I think you should write custom template tag that would take `request` as a parameter, and then print your parameter string back to template. – Tomasz Zieliński Jan 12 '10 at 09:28
  • Also, this doesn't seem to work with select boxes where you can select multiple options. – Liam May 12 '10 at 19:53
  • It does not violate DRY principle if you use template inheritance for the pagination – Guillaume Lebreton Sep 28 '18 at 14:40
9

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 %}
miku
  • 181,842
  • 47
  • 306
  • 310
6

One can create a context processor to use it wherever pagination is applied.

For example, in my_project/my_app/context_processors.py:

def getvars(request):
    """
    Builds a GET variables string to be uses in template links like pagination
    when persistence of the GET vars is needed.
    """
    variables = request.GET.copy()

    if 'page' in variables:
        del variables['page']

    return {'getvars': '&{0}'.format(variables.urlencode())}

Add the context processor to your Django project settings:

TEMPLATE_CONTEXT_PROCESSORS = (
    'django.contrib.auth.context_processors.auth',
    'django.contrib.messages.context_processors.messages',
    'django.core.context_processors.i18n',
    'django.core.context_processors.request',
    'django.core.context_processors.media',
    'django.core.context_processors.static',
     ...
    'my_project.my_app.context_processors.getvars',
)

Then, in your templates, you can use this when paginating:

<div class="row">
    {# Initial/backward buttons #}
    <div class="col-xs-4 col-md-4 text-left">
        <a href="?page=1{{ getvars }}" class="btn btn-rounded">{% trans 'first' %}</a>
        {% if page_obj.has_previous %}
            <a href="?page={{ page_obj.previous_page_number }}{{ getvars }}" class="btn btn-rounded">{% trans 'previous' %}</a>
        {% endif %}
    </div>

    {# Page selection by number #}
    <div class="col-xs-4 col-md-4 text-center content-pagination">
        {% for page in page_obj.paginator.page_range %}
            {% ifequal page page_obj.number %}
                <a class="active">{{ page }}</a>
            {% else %}
                <a href="?page={{ page }}{{ getvars }}">{{ page }}</a>
            {% endifequal %}
        {% endfor %}
    </div>

    {# Final/forward buttons #}
    <div class="col-xs-4 col-md-4 text-right">
        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}{{ getvars }}" class="btn btn-rounded">{% trans 'next' %}</a>
        {% endif %}
        <a href="?page={{ paginator.num_pages }}{{ getvars }}" class="btn btn-rounded">{% trans 'last' %}</a>
    </div>
</div>

Whatever GET variables you have in your request, they will be appended after the ?page= GET parameter.

José L. Patiño
  • 3,683
  • 2
  • 29
  • 28
6

Improvement of this by:

Use urlencode from django instead of urllib, to prevent UnicodeEncodeError error with unicode arguments.

Template tag:

from django.utils.http import urlencode

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

Template:

<!-- Pagination -->
<div class="pagination">
 <span class="step-links">
   {% if coupons.has_previous %}
    <a href="?{% url_replace page=objects.previous_page_number %}">Prev</a>
   {% endif %}
   <span class="current">
    Page {{ objects.number }} of {{ objects.paginator.num_pages }}
   </span>
   {% if objects.has_next %}
    <a href="?{% url_replace page=objects.next_page_number %}">Next</a>
   {% endif %}
  </span>
</div>
Community
  • 1
  • 1
Omid Raha
  • 9,862
  • 1
  • 60
  • 64
4

This is a simple way how I do it

In view :

path = ''
path += "%s" % "&".join(["%s=%s" % (key, value) for (key, value) in request.GET.items() if not key=='page' ])

Then in template:

href="?page={{ objects.next_page_number }}&{{path}}"
Armance
  • 5,350
  • 14
  • 57
  • 80
4

I had this problem while using django-bootstrap3. The (easy) solution without any template tags is using:

{% bootstrap_pagination page_obj extra=request.GET.urlencode %}

Took me a while to find this out... I finally did thanks to this post.

nspo
  • 1,488
  • 16
  • 21
4

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

kvothe__
  • 591
  • 4
  • 10
3

Another take on the url_encode solution, in this case as simplified by skoval00.

I had a few issues with that version. One, it didn't support Unicode encoding and two, it broke for filters with multiple of the same keys (like a MultipleSelect widget). Due to the .dict() conversion, all values but one are lost. My version supports unicode and multiple of the same key:

from django import template
from django.utils.html import mark_safe

register = template.Library()

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

    for kwarg in kwargs:
        try:
            query.pop(kwarg)
        except KeyError:
            pass

    query.update(kwargs)

    return mark_safe(query.urlencode())

This creates a QueryDict copy, then removes all keys that match kwargs (since update for a QueryDict adds instead of replacing). Mark_safe was needed due to a double encoding issue.

You would use it like this (don't forget to load the tags):

<a class="next" href="?{% url_replace p=objects.next_page_number%}">Next</a>

where ?p=1 is our pagination syntax in the View.

Apollo Data
  • 1,267
  • 11
  • 20
  • 1
    Btw, a practical aside if you have a lot of views with pagination: make a generic pagination template. Then you can just include that in each view where you want to paginate: `{% include "core/pagination.html" with objects=ads_list %}` objects is the generic name of whatever you are paginating for the general template and you can assign to it whatever it's called in this particular template (ads_list, in this case). – Apollo Data Apr 05 '17 at 00:40
3

@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}'
Lord Elrond
  • 13,430
  • 7
  • 40
  • 80
  • I used [this](https://stackoverflow.com/questions/2047622/how-to-paginate-django-with-other-get-variables/57899037#comment115168364_36288962) solution in the comment by @themissionmars. It looks simpler? – Anupam Oct 27 '21 at 17:08
2

Here's a useful custom template tag for constructing query strings.

<a href="?{% make_query_string page=obj_list.next_page_number %}">Next page</a>

If the URL is http://example.com/django/page/?search=sometext, the generated HTML should be something like:

<a href="?search=sometext&page=2">Next page</a>

More examples:

<!-- Original URL -->
<!-- http://example.com/django/page/?page=1&item=foo&item=bar -->

<!-- Add or replace arguments -->
{% make_query_string page=2 item="foo2" size=10 %}
<!-- Result: page=2&item=foo2&size=10 -->

<!-- Append arguments -->
{% make_query_string item+="foo2" item+="bar2" %}
<!-- Result: page=1&item=foo&item=bar&item=foo2&item=bar2 -->

<!-- Remove a specific argument -->
{% make_query_string item-="foo" %}
<!-- Result: page=1&item=bar -->

<!-- Remove all arguments with a specific name -->
{% make_query_string item= %}
<!-- Result: page=1 -->

Finally, the source code (written by me):

# -*- coding: utf-8 -*-
from django import template
from django.utils.encoding import force_text  # Django 1.5+ only

register = template.Library()


class QueryStringNode(template.Node):
    def __init__(self, tag_name, parsed_args, var_name=None, silent=False):
        self.tag_name = tag_name
        self.parsed_args = parsed_args
        self.var_name = var_name
        self.silent = silent

    def render(self, context):
        # django.core.context_processors.request should be enabled in
        # settings.TEMPLATE_CONTEXT_PROCESSORS.
        # Or else, directly pass the HttpRequest object as 'request' in context.
        query_dict = context['request'].GET.copy()
        for op, key, value in self.parsed_args:
            if op == '+':
                query_dict.appendlist(key, value.resolve(context))
            elif op == '-':
                list_ = query_dict.getlist(key)
                value_ = value.resolve(context)
                try:
                    list_.remove(value_)
                except ValueError:
                    # Value not found
                    if not isinstance(value_, basestring):
                        # Try to convert it to unicode, and try again
                        try:
                            list_.remove(force_text(value_))
                        except ValueError:
                            pass
            elif op == 'd':
                try:
                    del query_dict[key]
                except KeyError:
                    pass
            else:
                query_dict[key] = value.resolve(context)
        query_string = query_dict.urlencode()
        if self.var_name:
            context[self.var_name] = query_string
        if self.silent:
            return ''
        return query_string


@register.tag
def make_query_string(parser, token):
    # {% make_query_string page=1 size= item+="foo" item-="bar" as foo [silent] %}
    args = token.split_contents()
    tag_name = args[0]
    as_form = False
    if len(args) > 3 and args[-3] == "as":
        # {% x_make_query_string ... as foo silent %} case.
        if args[-1] != "silent":
            raise template.TemplateSyntaxError(
                "Only 'silent' flag is allowed after %s's name, not '%s'." %
                (tag_name, args[-1]))
        as_form = True
        silent = True
        args = args[:-1]
    elif len(args) > 2 and args[-2] == "as":
        # {% x_make_query_string ... as foo %} case.
        as_form = True
        silent = False

    if as_form:
        var_name = args[-1]
        raw_pairs = args[1:-2]
    else:
        raw_pairs = args[1:]

    parsed_args = []
    for pair in raw_pairs:
        try:
            arg, raw_value = pair.split('=', 1)
        except ValueError:
            raise template.TemplateSyntaxError(
                "%r tag's argument should be in format foo=bar" % tag_name)
        operator = arg[-1]
        if operator == '+':
            # item+="foo": Append to current query arguments.
            # e.g. item=1 -> item=1&item=foo
            parsed_args.append(('+', arg[:-1], parser.compile_filter(raw_value)))
        elif operator == '-':
            # item-="bar": Remove from current query arguments.
            # e.g. item=1&item=bar -> item=1
            parsed_args.append(('-', arg[:-1], parser.compile_filter(raw_value)))
        elif raw_value == '':
            # item=: Completely remove from current query arguments.
            # e.g. item=1&item=2 -> ''
            parsed_args.append(('d', arg, None))
        else:
            # item=1: Replace current query arguments, e.g. item=2 -> item=1
            parsed_args.append(('', arg, parser.compile_filter(raw_value)))

    if as_form:
        node = QueryStringNode(tag_name, parsed_args,
                               var_name=var_name, silent=silent)
    else:
        node = QueryStringNode(tag_name, parsed_args)

    return node
Rockallite
  • 16,437
  • 7
  • 54
  • 48
2

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
artem343
  • 21
  • 3
1

@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'%}">
0

I would say generate the next and previous link from your controller, then pass it to the view and use it from there. I will give you an example (more like a pseudocode):

("next_link", "?param1="+param1+"&param2="+param2+"&page_nr="+(Integer.parseInt(page_nr)-1)

then in your view use it like this:

{% if contacts.has_next %}
<a href="?page={{ contacts.next_link }}">next</a>
{% endif %}
sm13294
  • 563
  • 7
  • 23
0

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.

Tomasz Zieliński
  • 16,136
  • 7
  • 59
  • 83
0

You will need to return the GET as stated above. You can pass the GET request part of the url by calling

render_dict['GET'] = request.GET.urlencode(True)
return render_to_response('search/search.html',
                          render_dict,
                          context_instance=RequestContext(request))

you can then use this in the template to build your URL e.g.

href="/search/client/{{ page.no }}/10/?{{ GET }}
ajaali
  • 860
  • 12
  • 16
0

With Django's Pagination - preserving the GET params is simple.

First copy the GET params to a variable (in view):

GET_params = request.GET.copy()

and send it to the template in via context dictionary:

return render_to_response(template,
                        {'request': request, 'contact': contact, 'GET_params':GET_params}, context_instance=RequestContext(request))

Second thing you need to do is use it, specify it in the url calls (href) in the template - an example (extending the basic pagination html to handle extra param condition):

{% if contacts.has_next %}
    {% if GET_params %}
        <a href="?{{GET_params.urlencode}}&amp;page={{ contacts.next_page_number }}">next</a>
    {% else %}
        <a href="?page={{ contacts.next_page_number }}">next</a>
    {% endif %}
{% endif %}

Source

Nabeel Ahmed
  • 18,328
  • 4
  • 58
  • 63
0

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 %}
rezz
  • 1
  • 1
0

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]
Patrik Radics
  • 43
  • 1
  • 5
-2

'path': request.get_full_path().rsplit('&page')[0],