В духе продолжения складывающейся традиции парных постов, я вслед за Максом Ищенко просто должен написать про "паджинацию" в Django.
Выборка
В Django уже есть небольшой объектик для постраничного вывода результатов. Чтобы его использовать, надо скормить ему табличку, из которой брать данные и, если надо, дополнительные параметры выборки. Покажу, как это выглядит, на примере все того же "Некого Музыкального Сервиса", про черновой стайлинг которого я недавно писал. Задача простая: вывести постраничный список альбомов, принадлежащих конкретному пользователю.
from django.models.main import albums
from django.core.paginator import ObjectPaginator
def albums(request):
# Создается объект
paginator = ObjectPaginator(
albums, # что выбирать
{'owner__id__exact':request.user.id}, # условие
20) # сколько на страницу
# Выбирается нужная страница по номеру из GET-параметров
page = int(request.GET.get('page','1'))
album_list = paginator.get_page(page-1)
# Выборка, паджинатор и номер сраницы отправляются в шаблон
return render_to_response('albums',{
'albums':album_list,
'paginator':paginator,
'page':page,
},context_instance=DjangoContext(request))
Немного комментариев:
Из GET'а выбирается номер страницы с 1 по умолчанию. Нумерация с 1 выбрана для того, чтобы URL читался проще для пользователя.
Сам же паджинатор считает страницы с нуля, поэтому
get_page(page-1)
.В шаблон передается не только получившийся список, но и номер страницы и сам объект паджинатора, чтобы выводить навигацию. Об этом дальше.
Если вы не пишите обработчик запроса вручную, а пользуетесь generic views, то там тоже можно задать постраничный вывод:
(r'^user/albums/$',
'django.views.generic.list_detail.object_list',
{'app_label':'myapp','module_name':'albums','paginate_by':20})
В шаблоне будет доступна куча переменных с текущей страницей, общим количеством страниц, соседними страницами и т.п. Перечислять не буду, они все есть в документации.
Оформление
Выдать объекты в шаблон — это еще не все. Я все время повторяю один из принципов Django, что он старается не навязывать никакого определенного HTML кода. В случае с постраничным выводом это особенно правильно, потому что вариантов нарисовать постраничную навигацию — множество.
Делать это я обязательно рекомендую не в самом шаблоне, а в виде отдельного шаблонного тега, потому что вы будете использовать это в очень многих местах. А чтобы слова "сделать свой тег" не отпугивали, я напишу его для вас прямо здесь :-).
Сначала надо сделать в директории приложения директорию "templatetags
", в которую поместить два файла:
__init__.py
, пустой файл, чтобы Питон воспринимал эту директорию как пакет<имя_приложения>.py
, тот, в котором и будут ваши теги
Для НМС я написал пока очень базовый тег, который умеет только выводить "n стр. из m" и ссылки на соседние страницы, если они есть. Вот весь код:
@register.simple_tag(takes_context=True)
def paginator(context):
paginator = context['paginator']
page = context['page']
# Делаются линки на соседние страницы, если они есть
if paginator.has_next_page(page-1):
next_link = '<a href="?page=%s" class="next">вперед →</a>'%page+1
else:
next_link = ''
if paginator.has_previous_page(page-1):
previous_link = '<a href="?page=%s" class="previous">← назад</a> '%page-1
else:
previous_link = ''
# Составляется финальный код
return '''
<div class="paginator">
%s
<span class="pages">стр. %s из %s</span>
%s
</div>'''%(previous_link, page, paginator.pages, next_link)
И вот так он вызывается в шаблоне:
{% paginator %}
Необходимые комментарии:
@register.simple_tag(takes_context=True)
— это заклинание (декоратор), которое регистрирует в шаблонной системе следующую за ним функцию в виде тега с тем же названием. Оно избавляет от рутины по копанию во внутренней механике парсинга и подстановки значений шаблонов. (Правда в таком виде оно таки не работает, см. комментарий.)Название переменных "page" и "paginator" зашиты жестко в код. Это намерено, потому что так проще, а ситуация, когда бы захотелось их вдруг назвать по-другому, представляется мне сильно надуманной. Появится — поменяю.
Весь смысл объекта "paginator" виден как раз здесь, у него есть удобные атрибуты "has_next_page()", "has_previous_page()", "pages", которые не надо никак вычислять.
Для новичков в Питоне: "%s" в строках — это те места, в которые будут подставлены переменные, перечисленные после строки после знака "%".
На самом деле, реальный тег в проекте все таки не такой простой. Мне понадобилось сделать два варианта вывода страниц: обычный и для архивов, где понятия "вперед" и "назад", как известно, прихотливо меняются. Кому интересно, можно взять файлик "paginator_tags.zip" и использовать в качестве основы в своем проекте.
Пустые списки
У Django'вского паджинатора есть одна неочевидная особенность: когда в выборке объектов нет данных, он не отдает пустой список, а выкидывает некрасивую ошибку. Это довольно неприятно, потому что при отладке у вас обычно есть какой-то набор тестовых данных, и с ним все работает хорошо. Но как только код попадает в другое место, где данных еще нет, там оно все и вылезает :-).
Обходится это просто, прямо в том месте, где идет выборка данных:
from django.core.paginator import ObjectPaginator, InvalidPage
...
try:
album_list = paginator.get_page(page-1)
except InvalidPage:
album_list = []
Это можно при желании даже в отдельную функцию завернуть.
Комментарии: 10
Спасибо за разъяснения на пальцах. Ваши пальцы нам очень нужны (:
Да, paginator у turbogears монструозный.. (=
Иван, продемострируйте пожалуйста линки, которые генерирует этот паджинатор. Просто первый раз встречаюсь с этим "термином" :) По-сути, если я понял, что-о типа index.html?page=123
Скоро попробую написать статью как раз про нумерацию страниц. Уже мыслей куча :)
Ну сам этот объектик линки не генерит, он нужен для того, чтобы выбирать нужную страницу из базы и предоставлять удобные функции типа "сколько всего страниц", "есть ли следующая/предыдущая страница".
Как организовывать линки — это полностью зависит от программиста. Сейчас у меня они выглядят как "/user/albums/?page=2". Но можно, например, сделать "/user/albums/pages/2/" или "/user/albums,(page_2)". По-разному, в общем, больное воображение проявлять можно :-)
ну вот я и вижу, что ?page=2. потому что get :)
эх, так чешутся руки статейку написать!! вот только сессию закрою и пойду :)
Иван, спасибо за Ваши статьи о Django! Именно благодаря им я открыл для себя этот крутейший фреймворк.
Django - шикарнейшая вещь!!! Прошёл туториал, что на сайте djangoproject, и это было для меня прозрением, которое в корне изменило моё видение удобных фреймворков ;-)
После знакомства с Django, понимаешь что PHP - это игрушка для детей дошкольного возраста :-D Шучу, конечно. Наша команда писала на PHP сложные системы разного направления, но вопрос заключается в том, какой ценой удавались такие вещи как динамическая маршрутизация для IP-телефонии или intelligent server для управления DNS :)
Ещё раз спасибо Вам за агитацию! =)
Оказывается, я поспешил с примером кода шаблонного тега, не проверив его. Вот это место:
... не работает. Я его увидел в Traс'е, но вот в код это так и не включили. Так что, "simple_tag" остается действительно очень простым. Если в теге нужен контекст, то придется рисовать классы (что муторно).
P.S. Но вот файлик paginator_tags.zip работает.
Mkdir, а теперь откройте для себя TurboGears :)
А потом закройте обратно и больше не вспоминайте. q:
Спасибо за интересный пост! Только начинаю осваивать Django, для меня в нем все ново, но уже успел оценить его очаровательную элегантность и продуманность.
Пытался реализовать твой пример с Paginator на практике, натолкнулся на несколько непонятных моментов. Но ничего, осилил. Вот, рассказываю:
Сам файл с исходником Paginator'а называю, к примеру, app_custom.py и кидаю туда же. Вот этот файл(чуточку измененный файл Ивана):
from django.template import Library, Node
register = Library()
class PaginatorNode(Node):
def render(self,context): paginator=context['paginator'] page_number=context['page'] class_name='paginator' next_text='Next' previous_text='Previous' if paginator.has_next_page(page_number-1): next_link='%s '%(page_number+1,next_text) else: next_link='' pages_text='page %s of %s '%(page_number,paginator.pages) if paginator.has_previous_page(page_number-1): previous_link='%s '%(page_number-1,previous_text) else: previous_link='' return '%s %s %s'%(class_name,previous_link,pages_text,next_link)
@register.tag def paginator(parser, token): return PaginatorNode() paginator = register.tag(paginator)
Где-то в начале template'a указываю...
...и в нужном месте template'a пишу
После этого у меня все заработало.
Еще раз спасибо за полезную наводку!