Декоратор для удобного постраничного разбиения

Стандартное решение предлагаемое джанго для постраничного разбиения списка объектов - это класс django.core.paginator.ObjectPaginator

Здесь можно посмотреть примеры использования ObjectPaginator [djangoproject.com]

Однако, использование ObjectPaginator в голом виде не совсем удобно. В самом деле, нужно:
- извлечь номер страницы из GET-данных или URL запроса
- создать QuerySet объектов для пагинации
- создать ObjectPaginator, передав ему этот QuerySet
- получить объекты текущей страницы, путём вызова метода get_page у пагинатора

Вся эта рутина так и просится в декоратор. Без лишних слов, собственно, код декоратора )


def paged(paged_list_name, per_page=10):
def decorator(func):
def wrapper(request, *args, **kwargs):
result = func(request, *args, **kwargs)
if not isinstance(result, dict):
return result
try:
page = int(request.GET.get('page', 1))
except ValueError:
page = 1
from django.core.paginator import ObjectPaginator
paginator = ObjectPaginator(result['paged_qs'], per_page)
result[paged_list_name] = paginator.get_page(page - 1)
result['page'] = page
result['pages'] = paginator.pages
return result
return wrapper
return decorator

def paged(paged_list_name, per_page=10):
def decorator(func):
def wrapper(request, *args, **kwargs):
result = func(request, *args, **kwargs)
if not isinstance(result, dict):
return result
try:
page = int(request.GET.get(‘page’, 1))
except ValueError:
page = 1
from django.core.paginator import ObjectPaginator
paginator = ObjectPaginator(result[‘paged_qs’], per_page)
result[paged_list_name] = paginator.get_page(page - 1)
result[‘page’] = page
result[‘pages’] = paginator.pages
return result
return wrapper
return decorator

Поясню, как я его использую. Дело в том, что все мои view возвращают не объект класса HttpResponse, но обыкновенный словарь, который потом обрабатывается декоратором render_to. Подробнее об этом вы можете прочитать в этой статье [web-brains.com]

Ну а в декораторе paged я просто дополняю этот словарь, который вернула моя view. Я кладу в словарь элементы с ключами page, pages и список объектов. Имя элемента со списком задаётся в первом позиционном аргументе декоратора. Чтобы декоратор работал, нужно из view вернуть QuerySet объектов для пагинации в элементе с ключом paged_qs.

Код ещё сырой, я его написал час назад. До этого я использова не такой навороченный декоратор. Если у вас есть замечания и предложения, с радостью выслушаю.

Ну, и, конечно, было бы неплохо, чтобы список со страничками сам строился ;-)
В этом нам поможет следующий inclusion tag.


@register.inclusion_tag('site/pagination.html',takes_context=True)
def pagination(context,adjacent_pages=5):
page_list = range(
max(1,context['page'] - adjacent_pages),
min(context['pages'],context['page'] + adjacent_pages) + 1)
lower_page = None
higher_page = None

if not 1 == context['page']:
lower_page = context['page'] - 1

if not 1 in page_list:
page_list.insert(0,1)
if not 2 in page_list:
page_list.insert(1,'.')

if not context['pages'] == context['page']:
higher_page = context['page'] + 1

if not context['pages'] in page_list:
if not context['pages'] - 1 in page_list:
page_list.append('.')
page_list.append(context['pages'])
get_params = '&'.join(['%s=%s' % (x[0],','.join(x[1])) for x in
context['request'].GET.iteritems() if not x[0] == 'page'])
if get_params:
get_params = get_params + '&'

return {
'get_params': get_params,
'lower_page': lower_page,
'higher_page': higher_page,
'page': context['page'],
'pages': context['pages'],
'page_list': page_list}

@register.inclusion_tag(‘site/pagination.html’,takes_context=True)
def pagination(context,adjacent_pages=5):
page_list = range(
max(1,context[‘page’] - adjacent_pages),
min(context[‘pages’],context[‘page’] + adjacent_pages) + 1)
lower_page = None
higher_page = None

if not 1 == context[‘page’]:
lower_page = context[‘page’] - 1

if not 1 in page_list:
page_list.insert(0,1)
if not 2 in page_list:
page_list.insert(1,’.’)

if not context[‘pages’] == context[‘page’]:
higher_page = context[‘page’] + 1

if not context[‘pages’] in page_list:
if not context[‘pages’] - 1 in page_list:
page_list.append(‘.’)
page_list.append(context[‘pages’])
get_params = ‘&’.join([‘%s=%s’ % (x[0],’,’.join(x[1])) for x in
context[‘request’].GET.iteritems() if not x[0] == ‘page’])
if get_params:
get_params = get_params + ‘&’

return {
‘get_params’: get_params,
‘lower_page’: lower_page,
‘higher_page’: higher_page,
‘page’: context[‘page’],
‘pages’: context[‘pages’],
‘page_list’: page_list}


А это pagination.html


<div class="pagination">
{% ifnotequal pages 1 %}
Страницы:
{% if lower_page %}
<a href="?page={{ lower_page }}">&laquo;</a>
{% endif %}
{% for number in page_list %}
{% ifequal number "." %}...
{% else %}
{% ifequal number page %}{{ number }}{% endifequal %}
{% ifnotequal number page %}
<a href="?page={{ number }}">{{ number }}</a>
{% endifnotequal %}
{% endifequal %}
{% endfor %}
{% if higher_page %}
<a href="?page={{ higher_page }}">&raquo;</a>
{% endif %}
{% endifnotequal %}
</div>

<div class=”pagination”>
{% ifnotequal pages 1 %}
Страницы:
{% if lower_page %}
<a href=”?page={{ lower_page }}”>&laquo;</a>
{% endif %}
{% for number in page_list %}
{% ifequal number “.” %}…
{% else %}
{% ifequal number page %}{{ number }}{% endifequal %}
{% ifnotequal number page %}
<a href=”?page={{ number }}”>{{ number }}</a>
{% endifnotequal %}
{% endifequal %}
{% endfor %}
{% if higher_page %}
<a href=”?page={{ higher_page }}”>&raquo;</a>
{% endif %}
{% endifnotequal %}
</div>
Add post to:   Delicious Reddit Slashdot Digg Technorati Google
Make comment

Comments

Отлично, заюзал оба декоратора :)

Чтобы не заморачиваься с paged_qs, предлагаю заменить:

paginator = Paginator(result['paged_qs'], real_per_page)

На:

paginator = Paginator(result[paged_list_name], real_per_page)

Гм, действительно. Только вот пока сомневаюсь, юзать ли это улучшение в моих проектах. Я уже наделал кучу на старом декораторе и, видимо, начну путаться, если сейчас заюзаю новую версию в некоторых проектах )

Столкнулся с ситуацией, когда страниц много и при больших цифрах пейджер не вмещается в строку. А при уменьшении adjacent_pages не хорошо на границах. Захотелось, чтобы на границах adjacent_pages увеличивался.

Заменил:

page_list = range(
    max(1,context['page'] — adjacent_pages),
    min(context['pages'],context['page'] + adjacent_pages) + 1)

На:

left= max(1,context['page'] — adjacent_pages)
right= min(context['pages'], adjacent_pages * 2 + left) + 1
left=  max(1,right — adjacent_pages * 2) — 1
page_list = range(left, right)

В резуальтате получается что-то типа:

1 2 3 4 5 6 7 ... 15220 >>>

<<< 1 … 15208 15209 15210 15211 15212 15213 15214 … 15220 >>>

<<< 1 ... 15214 15215 15216 15217 15218 15219 15220

Ох, попозже помедирую. Ты, кстати, а аську не писал? Вроде приходил запрос авторизации, но аську у меня вышибло, так что тока жабер работает.

Не. Хотел было, но подумал, что тебе сейчас не до меня :)

Где-то тут бага: get_params = ‘&’.join([‘%s=%s’ % (x[0],’,’.join(x[1])) for x in context[‘request’].GET.iteritems() if not x[0] == ‘page’])

Пипец, ну и месиво же я полтора года назад писал ))

Как-нить соберусь на недельке и перепишу этот древнючий код, тем более, он в pybb используется.

О, респект, нашёл. В этом кусочке ’,’.join нужно заменить на ’’.join

В {{ get_params }}, наверное, нужно |safe добавить.

И ещё херь непонятная. Вот, глянь, что с параметром t сталося: http://pydev.ru/forum/topic/318/?t=test

Кстати, в плане сео пажинация не идеальна.

В общем случае нужно равномерное распределение пиара по страницам. Тогда, как минимум, ссылки на первую и последнюю страницы имеет смысл выводить через js. Иначе на них будет скапливаться большой пиар, они же на каждой паге отображаются.

Required. 30 chars of fewer.

Required.

captcha image Please, enter symbols, which you see on the image