Пагинация в Django
Пагинация разбивает большие списки данных на страницы для улучшения производительности и пользовательского опыта. Правильная реализация пагинации критически важна для сайтов с большим количеством контента.
Базовая пагинация во views
1from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
2from django.shortcuts import render, get_object_or_404
3from django.db.models import Q
4from .models import Book, Author, Category
5from django.http import JsonResponse
6import json
7
8def book_list(request):
9 """Список книг с пагинацией"""
10 # Получаем параметры фильтрации
11 search_query = request.GET.get('q', '')
12 category_id = request.GET.get('category', '')
13 author_id = request.GET.get('author', '')
14 sort_by = request.GET.get('sort', '-published_date')
15
16 # Базовый QuerySet
17 books = Book.objects.select_related('author').prefetch_related('categories')
18
19 # Применяем фильтры
20 if search_query:
21 books = books.filter(
22 Q(title__icontains=search_query) |
23 Q(description__icontains=search_query) |
24 Q(author__name__icontains=search_query)
25 )
26
27 if category_id:
28 books = books.filter(categories__id=category_id)
29
30 if author_id:
31 books = books.filter(author__id=author_id)
32
33 # Сортировка
34 if sort_by == 'price':
35 books = books.order_by('price')
36 elif sort_by == '-price':
37 books = books.order_by('-price')
38 elif sort_by == 'title':
39 books = books.order_by('title')
40 else:
41 books = books.order_by('-published_date')
42
43 # Настройки пагинации
44 paginator = Paginator(books, 12) # 12 книг на страницу
45 page_number = request.GET.get('page')
46
47 try:
48 page_obj = paginator.get_page(page_number)
49 except PageNotAnInteger:
50 # Если страница не является числом, показываем первую
51 page_obj = paginator.page(1)
52 except EmptyPage:
53 # Если страница выходит за пределы, показываем последнюю
54 page_obj = paginator.page(paginator.num_pages)
55
56 # Контекст для шаблона
57 context = {
58 'page_obj': page_obj,
59 'books': page_obj.object_list,
60 'search_query': search_query,
61 'category_id': category_id,
62 'author_id': author_id,
63 'sort_by': sort_by,
64 'categories': Category.objects.all(),
65 'authors': Author.objects.all(),
66 'total_books': paginator.count,
67 'total_pages': paginator.num_pages,
68 }
69
70 return render(request, 'books/book_list.html', context)
71
72# Class-based view с пагинацией
73from django.views.generic import ListView
74from django.core.paginator import Paginator
75
76class BookListView(ListView):
77 model = Book
78 template_name = 'books/book_list.html'
79 context_object_name = 'books'
80 paginate_by = 12
81 ordering = ['-published_date']
82
83 def get_queryset(self):
84 """Кастомизация QuerySet с фильтрацией"""
85 queryset = Book.objects.select_related('author').prefetch_related('categories')
86
87 # Поиск
88 search_query = self.request.GET.get('q')
89 if search_query:
90 queryset = queryset.filter(
91 Q(title__icontains=search_query) |
92 Q(description__icontains=search_query)
93 )
94
95 # Фильтрация по категории
96 category_id = self.request.GET.get('category')
97 if category_id:
98 queryset = queryset.filter(categories__id=category_id)
99
100 return queryset
101
102 def get_context_data(self, **kwargs):
103 """Дополнительный контекст"""
104 context = super().get_context_data(**kwargs)
105 context.update({
106 'search_query': self.request.GET.get('q', ''),
107 'category_id': self.request.GET.get('category', ''),
108 'categories': Category.objects.all(),
109 'total_books': self.get_queryset().count(),
110 })
111 return context
Кастомные пагинаторы
Пагинатор с диапазоном страниц
1class CustomPaginator(Paginator):
2 """Кастомный пагинатор с диапазоном страниц"""
3
4 def get_page_range(self, current_page, window=2):
5 """Возвращает диапазон страниц для отображения"""
6 if self.num_pages <= 2 * window + 1:
7 # Если страниц мало, показываем все
8 return range(1, self.num_pages + 1)
9
10 # Вычисляем границы диапазона
11 start = max(1, current_page - window)
12 end = min(self.num_pages, current_page + window)
13
14 # Добавляем первую и последнюю страницу
15 page_range = []
16 if start > 1:
17 page_range.extend([1, '...'])
18
19 page_range.extend(range(start, end + 1))
20
21 if end < self.num_pages:
22 page_range.extend(['...', self.num_pages])
23
24 return page_range
25
26# Использование кастомного пагинатора
27def book_list_with_custom_pagination(request):
28 books = Book.objects.all()
29 paginator = CustomPaginator(books, 10)
30 page_number = request.GET.get('page', 1)
31
32 try:
33 page_obj = paginator.get_page(page_number)
34 page_range = paginator.get_page_range(page_obj.number)
35 except (PageNotAnInteger, EmptyPage):
36 page_obj = paginator.page(1)
37 page_range = paginator.get_page_range(1)
38
39 context = {
40 'page_obj': page_obj,
41 'page_range': page_range,
42 'books': page_obj.object_list,
43 }
44
45 return render(request, 'books/book_list.html', context)
46
47# Пагинатор с информацией о соседних страницах
48class SmartPaginator(Paginator):
49 """Умный пагинатор с информацией о соседних страницах"""
50
51 def get_page_info(self, current_page):
52 """Возвращает информацию о текущей странице и соседних"""
53 try:
54 page_obj = self.page(current_page)
55 except (PageNotAnInteger, EmptyPage):
56 page_obj = self.page(1)
57
58 # Информация о соседних страницах
59 has_previous = page_obj.has_previous()
60 has_next = page_obj.has_next()
61
62 if has_previous:
63 previous_page_number = page_obj.previous_page_number()
64 else:
65 previous_page_number = None
66
67 if has_next:
68 next_page_number = page_obj.next_page_number()
69 else:
70 next_page_number = None
71
72 # Диапазон страниц
73 page_range = self.get_page_range(page_obj.number)
74
75 return {
76 'page_obj': page_obj,
77 'has_previous': has_previous,
78 'has_next': has_next,
79 'previous_page_number': previous_page_number,
80 'next_page_number': next_page_number,
81 'page_range': page_range,
82 'total_pages': self.num_pages,
83 'total_count': self.count,
84 'current_page': page_obj.number,
85 }
Пагинатор для бесконечной прокрутки
1class InfiniteScrollPaginator(Paginator):
2 """Пагинатор для бесконечной прокрутки"""
3
4 def get_page_data(self, page_number, items_per_page=20):
5 """Возвращает данные для конкретной страницы"""
6 start = (page_number - 1) * items_per_page
7 end = start + items_per_page
8
9 return {
10 'items': self.object_list[start:end],
11 'has_next': end < self.count,
12 'next_page': page_number + 1 if end < self.count else None,
13 'total_count': self.count,
14 'current_page': page_number,
15 }
16
17# View для бесконечной прокрутки
18def infinite_scroll_books(request):
19 """API для бесконечной прокрутки книг"""
20 if request.method == 'GET':
21 page_number = int(request.GET.get('page', 1))
22 items_per_page = int(request.GET.get('per_page', 20))
23
24 books = Book.objects.select_related('author').prefetch_related('categories')
25
26 # Применяем фильтры
27 category_id = request.GET.get('category')
28 if category_id:
29 books = books.filter(categories__id=category_id)
30
31 author_id = request.GET.get('author')
32 if author_id:
33 books = books.filter(author__id=author_id)
34
35 # Сортировка
36 sort_by = request.GET.get('sort', '-published_date')
37 if sort_by == 'price':
38 books = books.order_by('price')
39 elif sort_by == '-price':
40 books = books.order_by('-price')
41 else:
42 books = books.order_by('-published_date')
43
44 paginator = InfiniteScrollPaginator(books, items_per_page)
45
46 try:
47 page_data = paginator.get_page_data(page_number, items_per_page)
48
49 # Сериализуем данные
50 books_data = []
51 for book in page_data['items']:
52 books_data.append({
53 'id': book.id,
54 'title': book.title,
55 'author': book.author.name,
56 'price': str(book.price),
57 'cover_url': book.cover_image.url if book.cover_image else None,
58 'categories': [cat.name for cat in book.categories.all()],
59 'rating': book.get_average_rating(),
60 'review_count': book.reviews.count(),
61 })
62
63 return JsonResponse({
64 'success': True,
65 'books': books_data,
66 'has_next': page_data['has_next'],
67 'next_page': page_data['next_page'],
68 'total_count': page_data['total_count'],
69 'current_page': page_data['current_page'],
70 })
71
72 except Exception as e:
73 return JsonResponse({
74 'success': False,
75 'error': str(e)
76 }, status=500)
77
78 return JsonResponse({'error': 'Method not allowed'}, status=405)
Шаблоны для пагинации
Базовый шаблон пагинации
1<!-- templates/includes/pagination.html -->
2{% if page_obj.has_other_pages %}
3<nav aria-label="Навигация по страницам">
4 <ul class="pagination justify-content-center">
5 <!-- Кнопка "Предыдущая" -->
6 {% if page_obj.has_previous %}
7 <li class="page-item">
8 <a class="page-link" href="?{% if request.GET.urlencode %}{{ request.GET.urlencode }}&{% endif %}page={{ page_obj.previous_page_number }}" aria-label="Предыдущая">
9 <span aria-hidden="true">«</span>
10 </a>
11 </li>
12 {% else %}
13 <li class="page-item disabled">
14 <span class="page-link" aria-label="Предыдущая">
15 <span aria-hidden="true">«</span>
16 </span>
17 </li>
18 {% endif %}
19
20 <!-- Номера страниц -->
21 {% for page_num in page_obj.paginator.page_range %}
22 {% if page_num == page_obj.number %}
23 <li class="page-item active">
24 <span class="page-link">{{ page_num }}</span>
25 </li>
26 {% elif page_num > page_obj.number|add:'-3' and page_num < page_obj.number|add:'3' %}
27 <li class="page-item">
28 <a class="page-link" href="?{% if request.GET.urlencode %}{{ request.GET.urlencode }}&{% endif %}page={{ page_num }}">{{ page_num }}</a>
29 </li>
30 {% endif %}
31 {% endfor %}
32
33 <!-- Кнопка "Следующая" -->
34 {% if page_obj.has_next %}
35 <li class="page-item">
36 <a class="page-link" href="?{% if request.GET.urlencode %}{{ request.GET.urlencode }}&{% endif %}page={{ page_obj.next_page_number }}" aria-label="Следующая">
37 <span aria-hidden="true">»</span>
38 </a>
39 </li>
40 {% else %}
41 <li class="page-item disabled">
42 <span class="page-link" aria-label="Следующая">
43 <span aria-hidden="true">»</span>
44 </span>
45 </li>
46 {% endif %}
47 </ul>
48</nav>
49
50<!-- Информация о страницах -->
51<div class="text-center text-muted">
52 <small>
53 Страница {{ page_obj.number }} из {{ page_obj.paginator.num_pages }}
54 (Всего {{ page_obj.paginator.count }} элементов)
55 </small>
56</div>
57{% endif %}
Кастомный шаблон с диапазоном страниц
1<!-- templates/includes/smart_pagination.html -->
2{% if page_obj.has_other_pages %}
3<nav aria-label="Навигация по страницам">
4 <ul class="pagination pagination-lg justify-content-center">
5 <!-- Первая страница -->
6 {% if page_obj.number > 1 %}
7 <li class="page-item">
8 <a class="page-link" href="?{% if request.GET.urlencode %}{{ request.GET.urlencode }}&{% endif %}page=1" aria-label="Первая страница">
9 <i class="fas fa-angle-double-left"></i>
10 </a>
11 </li>
12 {% endif %}
13
14 <!-- Предыдущая страница -->
15 {% if page_obj.has_previous %}
16 <li class="page-item">
17 <a class="page-link" href="?{% if request.GET.urlencode %}{{ request.GET.urlencode }}&{% endif %}page={{ page_obj.previous_page_number }}" aria-label="Предыдущая страница">
18 <i class="fas fa-angle-left"></i>
19 </a>
20 </li>
21 {% endif %}
22
23 <!-- Диапазон страниц -->
24 {% for page_item in page_range %}
25 {% if page_item == '...' %}
26 <li class="page-item disabled">
27 <span class="page-link">{{ page_item }}</span>
28 </li>
29 {% elif page_item == page_obj.number %}
30 <li class="page-item active">
31 <span class="page-link">{{ page_item }}</span>
32 </li>
33 {% else %}
34 <li class="page-item">
35 <a class="page-link" href="?{% if request.GET.urlencode %}{{ request.GET.urlencode }}&{% endif %}page={{ page_item }}">{{ page_item }}</a>
36 </li>
37 {% endif %}
38 {% endfor %}
39
40 <!-- Следующая страница -->
41 {% if page_obj.has_next %}
42 <li class="page-item">
43 <a class="page-link" href="?{% if request.GET.urlencode %}{{ request.GET.urlencode }}&{% endif %}page={{ page_obj.next_page_number }}" aria-label="Следующая страница">
44 <i class="fas fa-angle-right"></i>
45 </a>
46 </li>
47 {% endif %}
48
49 <!-- Последняя страница -->
50 {% if page_obj.number < page_obj.paginator.num_pages %}
51 <li class="page-item">
52 <a class="page-link" href="?{% if request.GET.urlencode %}{{ request.GET.urlencode }}&{% endif %}page={{ page_obj.paginator.num_pages }}" aria-label="Последняя страница">
53 <i class="fas fa-angle-double-right"></i>
54 </a>
55 </li>
56 {% endif %}
57 </ul>
58</nav>
59
60<!-- Дополнительная информация -->
61<div class="row">
62 <div class="col-md-6">
63 <div class="d-flex justify-content-start">
64 <div class="btn-group" role="group">
65 <button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle" data-bs-toggle="dropdown">
66 {{ page_obj.paginator.per_page }} на странице
67 </button>
68 <ul class="dropdown-menu">
69 <li><a class="dropdown-item" href="?{% if request.GET.urlencode %}{{ request.GET.urlencode }}&{% endif %}per_page=10">10 на странице</a></li>
70 <li><a class="dropdown-item" href="?{% if request.GET.urlencode %}{{ request.GET.urlencode }}&{% endif %}per_page=20">20 на странице</a></li>
71 <li><a class="dropdown-item" href="?{% if request.GET.urlencode %}{{ request.GET.urlencode }}&{% endif %}per_page=50">50 на странице</a></li>
72 </ul>
73 </div>
74 </div>
75 </div>
76 <div class="col-md-6">
77 <div class="d-flex justify-content-end">
78 <small class="text-muted">
79 Показано {{ page_obj.start_index }}-{{ page_obj.end_index }}
80 из {{ page_obj.paginator.count }} элементов
81 </small>
82 </div>
83 </div>
84</div>
85{% endif %}
AJAX пагинация
JavaScript для AJAX пагинации
1// static/js/ajax_pagination.js
2class AjaxPagination {
3 constructor(container, options = {}) {
4 this.container = container;
5 this.options = {
6 url: options.url || window.location.pathname,
7 pageParam: options.pageParam || 'page',
8 perPageParam: options.perPageParam || 'per_page',
9 perPage: options.perPage || 20,
10 onPageLoad: options.onPageLoad || null,
11 ...options
12 };
13
14 this.currentPage = 1;
15 this.isLoading = false;
16
17 this.init();
18 }
19
20 init() {
21 // Обработчик клика по пагинации
22 this.container.addEventListener('click', (e) => {
23 if (e.target.classList.contains('page-link') && !e.target.parentElement.classList.contains('disabled')) {
24 e.preventDefault();
25 const page = e.target.dataset.page || e.target.getAttribute('href').match(/page=(\d+)/)?.[1];
26 if (page) {
27 this.loadPage(parseInt(page));
28 }
29 }
30 });
31
32 // Обработчик изменения количества элементов на странице
33 const perPageSelect = this.container.querySelector('.per-page-select');
34 if (perPageSelect) {
35 perPageSelect.addEventListener('change', (e) => {
36 this.options.perPage = parseInt(e.target.value);
37 this.loadPage(1);
38 });
39 }
40 }
41
42 async loadPage(page) {
43 if (this.isLoading) return;
44
45 this.isLoading = true;
46 this.showLoading();
47
48 try {
49 const url = new URL(this.options.url, window.location.origin);
50 url.searchParams.set(this.options.pageParam, page);
51 url.searchParams.set(this.options.perPageParam, this.options.perPage);
52
53 // Добавляем текущие GET параметры
54 const currentParams = new URLSearchParams(window.location.search);
55 for (const [key, value] of currentParams) {
56 if (key !== this.options.pageParam && key !== this.options.perPageParam) {
57 url.searchParams.set(key, value);
58 }
59 }
60
61 const response = await fetch(url, {
62 headers: {
63 'X-Requested-With': 'XMLHttpRequest'
64 }
65 });
66
67 if (!response.ok) {
68 throw new Error(`HTTP error! status: ${response.status}`);
69 }
70
71 const data = await response.json();
72
73 if (data.success) {
74 this.updateContent(data);
75 this.currentPage = page;
76 this.updateURL(page);
77
78 if (this.options.onPageLoad) {
79 this.options.onPageLoad(data, page);
80 }
81 } else {
82 throw new Error(data.error || 'Unknown error');
83 }
84
85 } catch (error) {
86 console.error('Error loading page:', error);
87 this.showError(error.message);
88 } finally {
89 this.isLoading = false;
90 this.hideLoading();
91 }
92 }
93
94 updateContent(data) {
95 // Обновляем содержимое
96 const contentContainer = document.querySelector(this.options.contentSelector || '.content-container');
97 if (contentContainer && data.html) {
98 contentContainer.innerHTML = data.html;
99 }
100
101 // Обновляем пагинацию
102 const paginationContainer = document.querySelector(this.options.paginationSelector || '.pagination-container');
103 if (paginationContainer && data.pagination) {
104 paginationContainer.innerHTML = data.pagination;
105 }
106
107 // Обновляем информацию о страницах
108 const infoContainer = document.querySelector(this.options.infoSelector || '.pagination-info');
109 if (infoContainer && data.info) {
110 infoContainer.innerHTML = data.info;
111 }
112 }
113
114 updateURL(page) {
115 const url = new URL(window.location);
116 url.searchParams.set(this.options.pageParam, page);
117
118 // Обновляем URL без перезагрузки страницы
119 window.history.pushState({}, '', url);
120 }
121
122 showLoading() {
123 const loadingEl = this.container.querySelector('.loading');
124 if (loadingEl) {
125 loadingEl.style.display = 'block';
126 }
127 }
128
129 hideLoading() {
130 const loadingEl = this.container.querySelector('.loading');
131 if (loadingEl) {
132 loadingEl.style.display = 'none';
133 }
134 }
135
136 showError(message) {
137 const errorEl = this.container.querySelector('.error-message');
138 if (errorEl) {
139 errorEl.textContent = message;
140 errorEl.style.display = 'block';
141 }
142 }
143}
144
145// Инициализация AJAX пагинации
146document.addEventListener('DOMContentLoaded', function() {
147 const paginationContainer = document.querySelector('.pagination-container');
148 if (paginationContainer) {
149 const pagination = new AjaxPagination(paginationContainer, {
150 url: '/books/',
151 perPage: 20,
152 contentSelector: '.books-grid',
153 paginationSelector: '.pagination-container',
154 infoSelector: '.pagination-info',
155 onPageLoad: function(data, page) {
156 // Дополнительные действия при загрузке страницы
157 console.log(`Страница ${page} загружена`);
158
159 // Обновляем счетчики
160 if (data.total_count) {
161 document.querySelector('.total-books').textContent = data.total_count;
162 }
163 }
164 });
165 }
166});
View для AJAX пагинации
1from django.template.loader import render_to_string
2from django.http import JsonResponse
3
4def ajax_book_list(request):
5 """AJAX view для списка книг с пагинацией"""
6 if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
7 # Получаем параметры
8 page = int(request.GET.get('page', 1))
9 per_page = int(request.GET.get('per_page', 20))
10
11 # Фильтрация и сортировка
12 books = Book.objects.select_related('author').prefetch_related('categories')
13
14 search_query = request.GET.get('q')
15 if search_query:
16 books = books.filter(
17 Q(title__icontains=search_query) |
18 Q(description__icontains=search_query)
19 )
20
21 category_id = request.GET.get('category')
22 if category_id:
23 books = books.filter(categories__id=category_id)
24
25 # Сортировка
26 sort_by = request.GET.get('sort', '-published_date')
27 if sort_by == 'price':
28 books = books.order_by('price')
29 elif sort_by == '-price':
30 books = books.order_by('-price')
31 else:
32 books = books.order_by('-published_date')
33
34 # Пагинация
35 paginator = Paginator(books, per_page)
36
37 try:
38 page_obj = paginator.get_page(page)
39 except (PageNotAnInteger, EmptyPage):
40 page_obj = paginator.page(1)
41
42 # Рендерим HTML для содержимого
43 content_html = render_to_string('books/includes/book_grid.html', {
44 'books': page_obj.object_list,
45 'request': request
46 })
47
48 # Рендерим HTML для пагинации
49 pagination_html = render_to_string('includes/smart_pagination.html', {
50 'page_obj': page_obj,
51 'request': request
52 })
53
54 # Рендерим HTML для информации
55 info_html = render_to_string('books/includes/pagination_info.html', {
56 'page_obj': page_obj,
57 'request': request
58 })
59
60 return JsonResponse({
61 'success': True,
62 'html': content_html,
63 'pagination': pagination_html,
64 'info': info_html,
65 'current_page': page_obj.number,
66 'total_pages': paginator.num_pages,
67 'total_count': paginator.count,
68 'has_next': page_obj.has_next(),
69 'has_previous': page_obj.has_previous(),
70 })
71
72 # Обычный GET запрос - показываем полную страницу
73 return book_list(request)
Оптимизация пагинации
Кэширование пагинированных результатов
1from django.core.cache import cache
2from django.utils.decorators import method_decorator
3from django.views.decorators.cache import cache_page
4
5class CachedBookListView(ListView):
6 model = Book
7 template_name = 'books/book_list.html'
8 context_object_name = 'books'
9 paginate_by = 20
10
11 def get_cache_key(self, page_number):
12 """Генерирует ключ кэша для страницы"""
13 # Создаем уникальный ключ на основе параметров
14 cache_params = {
15 'page': page_number,
16 'per_page': self.paginate_by,
17 'search': self.request.GET.get('q', ''),
18 'category': self.request.GET.get('category', ''),
19 'sort': self.request.GET.get('sort', ''),
20 }
21
22 # Сортируем параметры для консистентности
23 sorted_params = sorted(cache_params.items())
24 cache_key = f"book_list:{hash(str(sorted_params))}"
25
26 return cache_key
27
28 def get_queryset(self):
29 """Получает QuerySet с кэшированием"""
30 cache_key = self.get_cache_key(1) # Кэшируем первую страницу
31
32 # Пытаемся получить из кэша
33 cached_queryset = cache.get(cache_key)
34 if cached_queryset is not None:
35 return cached_queryset
36
37 # Если нет в кэше, создаем и кэшируем
38 queryset = Book.objects.select_related('author').prefetch_related('categories')
39
40 # Применяем фильтры
41 search_query = self.request.GET.get('q')
42 if search_query:
43 queryset = queryset.filter(
44 Q(title__icontains=search_query) |
45 Q(description__icontains=search_query)
46 )
47
48 category_id = self.request.GET.get('category')
49 if category_id:
50 queryset = queryset.filter(categories__id=category_id)
51
52 # Сортировка
53 sort_by = self.request.GET.get('sort', '-published_date')
54 if sort_by == 'price':
55 queryset = queryset.order_by('price')
56 elif sort_by == '-price':
57 queryset = queryset.order_by('-price')
58 else:
59 queryset = queryset.order_by('-published_date')
60
61 # Кэшируем на 5 минут
62 cache.set(cache_key, queryset, 300)
63
64 return queryset
65
66# Кэширование отдельных страниц
67@method_decorator(cache_page(60 * 15)) # 15 минут
68def cached_book_list(request):
69 """Кэшированная версия списка книг"""
70 return book_list(request)
Ленивая загрузка для больших списков
1from django.core.paginator import Paginator
2from django.db.models import Prefetch
3
4class LazyBookListView(ListView):
5 model = Book
6 template_name = 'books/book_list_lazy.html'
7 context_object_name = 'books'
8 paginate_by = 50 # Больше элементов на страницу для ленивой загрузки
9
10 def get_queryset(self):
11 """Оптимизированный QuerySet с ленивой загрузкой"""
12 queryset = Book.objects.select_related('author').prefetch_related(
13 Prefetch(
14 'categories',
15 queryset=Category.objects.only('name', 'slug')
16 ),
17 Prefetch(
18 'reviews',
19 queryset=Review.objects.only('rating').select_related('user')
20 )
21 )
22
23 # Применяем фильтры
24 search_query = self.request.GET.get('q')
25 if search_query:
26 queryset = queryset.filter(
27 Q(title__icontains=search_query) |
28 Q(description__icontains=search_query)
29 )
30
31 return queryset
32
33 def get_context_data(self, **kwargs):
34 """Дополнительный контекст для ленивой загрузки"""
35 context = super().get_context_data(**kwargs)
36
37 # Добавляем информацию для ленивой загрузки
38 context['lazy_loading'] = True
39 context['load_more_url'] = reverse('books:load_more')
40
41 return context
42
43# API для ленивой загрузки
44def load_more_books(request):
45 """API для загрузки дополнительных книг"""
46 if request.method == 'GET':
47 offset = int(request.GET.get('offset', 0))
48 limit = int(request.GET.get('limit', 20))
49
50 books = Book.objects.select_related('author').prefetch_related('categories')[offset:offset + limit]
51
52 # Сериализуем данные
53 books_data = []
54 for book in books:
55 books_data.append({
56 'id': book.id,
57 'title': book.title,
58 'author': book.author.name,
59 'price': str(book.price),
60 'cover_url': book.cover_image.url if book.cover_image else None,
61 'categories': [cat.name for cat in book.categories.all()],
62 })
63
64 return JsonResponse({
65 'success': True,
66 'books': books_data,
67 'has_more': len(books_data) == limit,
68 'next_offset': offset + limit,
69 })
70
71 return JsonResponse({'error': 'Method not allowed'}, status=405)
Лучшие практики
- Используй select_related и prefetch_related - для оптимизации запросов
- Кэшируй пагинированные результаты - особенно для часто запрашиваемых страниц
- Ограничивай количество элементов на странице - оптимально 20-50 элементов
- Сохраняй GET параметры при пагинации - используй request.GET.urlencode
- Добавляй информацию о страницах - показывай текущую страницу и общее количество
- Используй AJAX для динамической загрузки - для лучшего UX
- Оптимизируй изображения - используй lazy loading для изображений
- Добавляй индикаторы загрузки - показывай состояние загрузки страниц
Частые ошибки и их решения
Ошибка: "Page not an integer"
Ошибка: "N+1 queries" при пагинации
FAQ
Q: Как сохранить GET параметры при пагинации?
A: Используй request.GET.urlencode в ссылках пагинации или создай кастомный template tag.
Q: Можно ли создать бесконечную прокрутку?
A: Да, используй AJAX для загрузки дополнительных данных при прокрутке до конца страницы.
Q: Как кэшировать пагинированные результаты?
A: Используй Django cache framework и создавай уникальные ключи кэша для каждой страницы.
Q: Можно ли изменить количество элементов на странице?
A: Да, добавь параметр per_page в GET запрос и передавай его в Paginator.
Q: Как добавить сортировку с пагинацией?
A: Добавь параметр sort в GET запрос и применяй ordering к QuerySet перед пагинацией.
Q: Можно ли создать кастомный пагинатор?
A: Да, наследуйся от Paginator и переопредели нужные методы для кастомной логики.