Yet another paginator (digg style)

This code will throw deprecation warnings in newer Django checkouts – see the Paginator Update post for an improved version that should work with the recent trunk.

These days I had to implement pagination with Django for the first time. Actually, that’s not entirely true – but thus far it had always been simple cases where the object_list() generic view and next/previous links were good enough. This time around, I wanted something more advanced.

It turns out that Django’s pagination code is not that great. It does it’s core job very well, but it is not stateful in that it doesn’t know anything about the current page – instead, you have to pass that value along with each function call. Which means that out of the box it’s pretty much unusable from within a template. Also, I wanted a more advanced pagination that would display a sensible range of pages for direct selection – apparently, this is now officially called “digg-style”.

There is at least one such paginator for Django around, but it’s based on an inclusion tag – a concept I am not too fond of. Somehow, I don’t like the idea of Python code (the view) referring to a template, referring back to Python code (the tag), referring back to a template, and maybe going even further from there. I like knowledge of templates and their locations to be restricted to my view layer.

So I decided to give a different approach a shot, putting an object into the template context that could be used to build the pagination. I’ve posted it to DjangoSnippets.

Use it like this:

objects = MyModel.objects.all()
paginator = DiggPaginator(objects, 10, body=6, padding=2, page=7)
return render_to_response('template.html', {'paginator': paginator}

The paginator will provide one to three blocks of pages (first pages, last pages, and adjacents of the current page). Depending on the total number of pages, the active page and the paginator options, those blocks may be merged together). body is the size of the block that contains the currently active page, tail the number of pages in the leading and trailing blocks. padding determines the minimum number of options to the left or right of the active page. This is only relevant when it comes to expanding a tail block into the middle block, as the padding of the latter is defined by it’s size (body). The last option, margin, defines when two blocks are merged. It’s the minimum number of pages required between two blocks. If margin is 3 while the start block ends with page 9, and the middle block starts with 11, the two blocks will end up as one.

A template would look something like the following. Pretty much all the variables the object_list generic view provides are available as attributes.

{% if paginator.has_next %}{# pagelink paginator.next #}{% endif %}
{% for page in paginator.page_range %}
   {% if not page %} ...
   {% else %}{# pagelink page #}
   {% endif %}
{% endfor %}

Here is an example in action.

17 thoughts on “Yet another paginator (digg style)

  1. @Michael – Found your blog and I love the solution. Quite a bit A lot cleaner (w/ respect to separation of concerns) than the inclusion tag hackfest that I put together.

    Definitely a +1. 🙂

    Like

  2. Hi there,
    I’m a django noob but really need to use pagination and yours looks like a nice way of doing it.

    Where do i put the 200-300 or so lines of code to make this work?

    I currently have it in my views.py right below:
    from django.shortcuts import get_object_or_404, render_to_response
    #from django.core.paginator import ObjectPaginator
    from django.template import RequestContext
    from django.http import HttpResponseRedirect
    from sakushi.menu.models import Menu
    import math
    from django.core.paginator import
    ObjectPaginator as DjangoPaginator,
    InvalidPage
    from django.http import Http404

    def menu_list(request):
    menu_list = Menu.objects.all().order_by(‘name’)
    paginator = DiggPaginator(menu_list, 10, body=6, padding=2, page=7)
    return render_to_response(‘menu_list.html’, {‘paginator’: paginator}

    However I am getting the error:
    SyntaxError at /menu/
    invalid syntax (views.py, line 18)
    Request Method: GET
    Request URL: xxxxxx
    Exception Type: SyntaxError
    Exception Value: invalid syntax (views.py, line 18)
    Exception Location: /home/nushi/lib/python2.5/django/core/urlresolvers.py in _get_callback, line 125

    Line 18 is this:
    class Paginator(object):

    Any help is greatly appreciated,

    Thanks,
    Duncan

    Like

  3. Please ignore the previous comment, I missed a ) [told you I was a noob =)]

    However now I have the code correctly in views.py? I get the

    Page no found at /menu/ when previously this page worked?

    Like

  4. You can put the paginator code wherever you like (basic Python). In your views.py file should work, but usually you’d want to move utility code like that in a separate module or even a package.

    If the view that is now throwing an 404 worked before, then I can only assume it’s the paginator that throws it when the page number is beyond bounds. Try replacing “page=7” with “page=1” for testing.

    Like

  5. Changing the “page=7” to “page=1” did fix the 404 page not found error. It however came up with a new error saying:
    ‘ObjectPaginator’ object has no attribute ‘page_range’
    I’m running Python 2.5 and Django 0.96 and I guess this is the reason as page_range is a new piece of API.

    How would I convert this:

    def update_attrs(self):
    “””Override in descendants to set custom fields.”””
    ……
    self.page_range = self._paginator.page_range

    To use a combination of the API available to produce the list this new one would?
    e.g. I need something like [1, 8]

    Many thanks,
    Duncan

    Like

  6. Ok, hopefully I’ve cleared up the noob mistakes, and have a genuine question.

    I used to have a template that I passed “menu_list” to from my views file.

    I would iterate through this with a simple for loop

    {% for menu in menu_list %}
    {{ menu.description }}
    {% endfor %}

    I am passing my template:
    return render_to_response(‘menu/menu_list.html’, {‘paginator’: paginator, ‘page’: page})

    If I pass it menu_list (Menu.objects.all()) then my template just shows ALL the menu’s, not just the 5 its supposed to because it’s paginated.

    Where do I get the paginated version of menu_list from and how do I call it in my template?

    Any template examples (including the paginator links) would top this tutorial off very nicely.

    Thanks again,
    Duncan

    Like

  7. Duncan, as for the page_range error – not sure, I’ve never worked with 0.96. I suggest you look at the SVN version of the ObjectPaginator class. With some luck you can easily backport it to 0.96 – I would guess it should be pretty standalone piece of code.

    Note however too that there is a completely new paginator class in Django SVN now that does similar things as the Paginator base class here, but the two are not compatible. When I find the time I will have to adjust DiggPaginator to use Django’s new pagination feature.

    If you only use the DiggPaginator class, you can also just remove that line (or make it a comment), as the class generates it’s own page_range attribute, whereas the Paginator base class copies it from Django’s ObjectPaginator class.

    Regarding your second question: I think you are looking for the object_list attribute:

    {% for menu in paginator.object_list %}
    {{ menu.description }}
    {% endfor %}

    Like

  8. That should just be matter of HTML code generation – just replace the {# pagelink page #} in the blog post example with whatever matches your CSS. Here’s what I use in an application – probably not 100% digg style, but you can build on it:

     {% if paginator.is_paginated %}
        
            {% if paginator.has_previous %}«{% endif %}
            {% for page in paginator.page_range %}
                {% if not page %}
                    ...
                {% else %}
                    {% ifequal page paginator.page %}{{ page }}
    
                    {% else %}{{ page }}{% endifequal %}
                {% endif %}
            {% endfor %}
            {% if paginator.has_next %}»{% endif %}
             Page {{ paginator.page }} of {{ paginator.pages }}
        
        {% endif %}
    

    And the CSS:

    .pagination a, .pagination .current {
        padding: 2px 5px;
        border: 1px solid;
    }
    
    .pagination .current {
        background-color: white;
        color: black;
    }
    

    Like

  9. Yes I’m back again…

    I’ve been trying to write a custom view to incorporate your pagination code in whilst using a wrapper to use the generic view slug handling.

    This is my views.py: http://dpaste.com/hold/47175/

    I’ve looked at many examples of how to create a wrapper, I just have no idea of how I could combine your custom pagination with a wrapper for the generic slug handling for urls?

    urls.py: http://dpaste.com/hold/47171/

    Obviously the #’d line is what I would use for a generic view, I was trying to recreate the same using a custom view.

    Many thanks again,
    Duncan

    Like

  10. I’m not entirely sure what you want to accomplish, but I am also not very familiar with generic views. It seems you want to recreate the functionality of the “object_detail” view with a paginator added, which I am not sure how that would work anyway, since object_detail shows exactly ONE object, whereas the paginator is intended for a list of objects, of course. But in general, you can either try to copy the generic view code and modify it according to your needs, or call the generic view at the end of your own custom view, and return the generic views’ result.

    In any case, this is pretty much unrelated to the paginator, so I would suggest you take this to django-users or the IRC channel.

    One note: All the “request.GET.has_key” stuff seems highly unnecessary. The following should suffice:

    paginator = DiggPaginator(menu_list, 6, page=request.GET.get('page', 1))

    This will create a paginator object with the current page set to whatever is in the “page” parameter, and it’ll default to the first page if the parameter is missing.

    Like

  11. I tried the IRC channel, and so far most people weren’t able to help me because they didn’t understand the custom paginator. Your correct in that it will show the object detail, but that has a relationship with another model. Menu and MenuItem. Menu is the object in question, I have my generic list view which lists all menu’s available, and then the detail view will show a singular Menu which has many MenuItem’s, which is where I need the paginator.

    I’ll try the IRC channel again but thanks again for the help anyway.

    Like

  12. Then you’ll probably want to write a custom object_detail view, e.g. not wrap the built-in one. The issue is that in order to paginate the menu items you need to know the requested menu, but if you wrap object_detail, you pass it the slug/id, and it returns a http response. Your own code will never even get access the the actual menu object.

    I suggest to leave the custom paginator out of it when asking questions – it doesn’t appear to be related to the actual issue itself. Maybe you should first try to make it work with the standard paginator – you can always switch them out later on.

    Don’t forget the mailing list either – you’re often going to be more successful there then on IRC.

    Like

  13. Really great snippet!
    But Django throws warning when using the paginator. Django says: “The ObjectPaginator is deprecated. Use django.core.paginator.Paginator instead.”.
    Would be nice if you could port your code to the django.core.paginator.Paginator, because else it presumeable won’t work in future.

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s