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
- Template tag - list punctuation for a list of items by shapiromatron 10 months, 2 weeks ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 3 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
- Help text hyperlinks by sa2812 1 year, 7 months ago
Comments
Very useful. I've made it unicode-safe (replace all
str()
withunicode()
and the__repr__()
with__unicode__()
, iterate on chunks sorted by key instead of iterating onstring.ascii_uppercase
, don't usestr().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.#
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 %}
#
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?
#
@datakid23
what You talking about is not a pagination but a filtration
#
You can look at #2025 ;]
#
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?
#
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 )
#
@HM it is possible to you to update the unicode stuff?? It would be nice ;)
#
Please login first before commenting.