Login

Pagination/Filtering Alphabetically

Author:
zain
Posted:
March 10, 2009
Language:
Python
Version:
1.0
Score:
9 (after 9 ratings)

This allows you to create an alphabetical filter for a list of objects; e.g. Browse by title: A-G H-N O-Z. See this entry in Yahoo's design pattern library for more info.

NamePaginator works like Django's Paginator. You pass in a list of objects and how many you want per letter range ("page"). Then, it will dynamically generate the "pages" so that there are approximately per_page objects per page.

By dynamically generating the letter ranges, you avoid having too many objects in some letter ranges and too few in some. If your list is heavy on one end of the letter range, there will be more pages for that range.

It splits the pages on letter boundaries, so not all the pages will have exactly per_page objects. However, it will decide to overflow or underflow depending on which is closer to per_page.

NamePaginator Arguments:

object_list: A list, dictionary, QuerySet, or something similar.

on: If you specified a QuerySet, this is the field it will paginate on. In the example below, we're paginating a list of Contact objects, but the Contact.email string is what will be used in filtering.

per_page: How many items you want per page.

Examples:

>>> paginator = NamePaginator(Contacts.objects.all(), \
... on="email", per_page=10)

>>> paginator.num_pages
4
>>> paginator.pages
[A, B-R, S-T, U-Z]
>>> paginator.count
36

>>> page = paginator.page(2)
>>> page
'B-R'
>>> page.start_letter
'B'
>>> page.end_letter
'R'
>>> page.number
2
>>> page.count
8

In your view, you have something like:

contact_list = Contacts.objects.all()
paginator = NamePaginator(contact_list, \
    on="first_name", per_page=25)

try:
    page = int(request.GET.get('page', '1'))
except ValueError:
    page = 1

try:
    page = paginator.page(page)
except (InvalidPage):
    page = paginator.page(paginator.num_pages)

return render_to_response('list.html', {"page": page})

In your template, have something like:

{% for object in page.object_list %}
...
{% endfor %}

<div class="pagination">
    Browse by title:
    {% for p in page.paginator.pages %}

      {% if p == page %}
          <span class="selected">{{ page }}</span>
      {% else %}
          <a href="?page={{ page.number }}">
              {{ page }}
          </a>
      {% endif %}

    {% endfor %}
</div>

It currently only supports paginating on alphabets (not alphanumeric) and will throw an exception if any of the strings it is paginating on are blank. You can fix either of those shortcomings pretty easily, though.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
import string
from django.core.paginator import InvalidPage, EmptyPage

class NamePaginator(object):
    """Pagination for string-based objects"""
    
    def __init__(self, object_list, on=None, per_page=25):
        self.object_list = object_list
        self.count = len(object_list)
        self.pages = []
        
        # chunk up the objects so we don't need to iterate over the whole list for each letter
        chunks = {}
        
        for obj in self.object_list:
            if on: obj_str = str(getattr(obj, on))
            else: obj_str = str(obj)
            
            letter = str.upper(obj_str[0])
            
            if letter not in chunks: chunks[letter] = []
            
            chunks[letter].append(obj)
        
        # the process for assigning objects to each page
        current_page = NamePage(self)
        
        for letter in string.ascii_uppercase:
            if letter not in chunks: 
                current_page.add([], letter)
                continue
            
            sub_list = chunks[letter] # the items in object_list starting with this letter
            
            new_page_count = len(sub_list) + current_page.count
            # first, check to see if sub_list will fit or it needs to go onto a new page.
            # if assigning this list will cause the page to overflow...
            # and an underflow is closer to per_page than an overflow...
            # and the page isn't empty (which means len(sub_list) > per_page)...
            if new_page_count > per_page and \
                    abs(per_page - current_page.count) < abs(per_page - new_page_count) and \
                    current_page.count > 0:
                # make a new page
                self.pages.append(current_page)
                current_page = NamePage(self)
            
            current_page.add(sub_list, letter)
        
        # if we finished the for loop with a page that isn't empty, add it
        if current_page.count > 0: self.pages.append(current_page)
        
    def page(self, num):
        """Returns a Page object for the given 1-based page number."""
        if len(self.pages) == 0:
            return None
        elif num > 0 and num <= len(self.pages):
            return self.pages[num-1]
        else:
            raise InvalidPage
    
    @property
    def num_pages(self):
        """Returns the total number of pages"""
        return len(self.pages)

class NamePage(object):
    def __init__(self, paginator):
        self.paginator = paginator
        self.object_list = []
        self.letters = []
    
    @property
    def count(self):
        return len(self.object_list)
    
    @property
    def start_letter(self):
        if len(self.letters) > 0: 
            self.letters.sort(key=str.upper)
            return self.letters[0]
        else: return None
    
    @property
    def end_letter(self):
        if len(self.letters) > 0: 
            self.letters.sort(key=str.upper)
            return self.letters[-1]
        else: return None
    
    @property
    def number(self):
        return self.paginator.pages.index(self) + 1
    
    def add(self, new_list, letter=None):
        if len(new_list) > 0: self.object_list = self.object_list + new_list
        if letter: self.letters.append(letter)
    
    def __repr__(self):
        if self.start_letter == self.end_letter:
            return self.start_letter
        else:
            return '%c-%c' % (self.start_letter, self.end_letter)

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 10 months, 2 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 3 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
  5. Help text hyperlinks by sa2812 1 year, 7 months ago

Comments

HM (on March 21, 2009):

Very useful. I've made it unicode-safe (replace all str() with unicode() and the __repr__() with __unicode__(), iterate on chunks sorted by key instead of iterating on string.ascii_uppercase, don't use str().upper() but <obj>.upper() instead) and more complete. Working on putting objects starting with numbers or symbols (in the unicode sense) in their own chunk too.

#

rm (on July 14, 2009):

Thanks for the snippet and thanks to HM for the unicode-ification. Said that, there are a couple of typos in the template: need ifequal instead if and p instead of page. Should be:

{% ifequal p page %}

<span class="selected">{{ page }}</span>

{% else %}

<a href="?page={{ p.number }}">{{ p }}</a>

{% endifequal %}

#

datakid23 (on April 28, 2010):

How hard would it be to change the pagination a little to offer 26 pages, 1 per letter, instead of x pages, 25 (or y) entries per page (across letters)? Is this a good place to start, or should I be using another alg?

#

slafs (on May 18, 2010):

@datakid23

what You talking about is not a pagination but a filtration

#

slafs (on May 18, 2010):

You can look at #2025 ;]

#

mhulse (on June 29, 2010):

HM, just curious if you ever finished working on your version of the code? If so, would you ever consider posting it for others to learn from?

#

vemubalu (on August 30, 2010):

Hi

It would be good to have a configurable parameter which if specified would accordingly paginate alphabetically or in groups

For example I want the whole letters to be displayed (Even return blank page if there are no objects for the alphabet searched )

#

andreslucena (on November 21, 2010):

@HM it is possible to you to update the unicode stuff?? It would be nice ;)

#

Please login first before commenting.