Использование Q-объекта и Paginator вместе в Django
Я создал View, который фильтрует данные по поисковому запросу, заданному в текстовом поле. Также я использовал Paginator для отображения данных, разделенных на страницы.
Моя проблема заключается в том, что когда я фильтрую данные с помощью объекта Q, а затем пытаюсь перейти к постраничной фильтрации, нажав на кнопку next, все данные обновляются.
Когда я ищу текст по объекту Q, URL становится http://127.0.0.1:8000/mael/parties/?q=keyword
После нажатия следующей кнопки URL становится http://127.0.0.1:8000/mael/parties/?page=2
Когда я вручную изменяю URL http://127.0.0.1:8000/mael/parties/?q=keyword&page=2, тогда это работает. Но я не знаю, как это сделать в коде.
Возможно ли совместное использование поиска объектов Q и пагинации?
Мое мнение
from mael.models import PartyTotalBillsView
from django.views.generic import ListView
from django.db.models import Q
from django.http import HttpResponseRedirect
class PartyListView(ListView):
paginate_by = 2
model = PartyTotalBillsView
def parties(request):
# Show all records or searched query record
search_text = request.GET.get('q','')
try:
if search_text:
queryset = (Q(party_name__icontains=search_text))
party_list = PartyTotalBillsView.objects.filter(queryset).order_by('party_name')
else:
# Show all data if empty keyword is entered
party_list = PartyTotalBillsView.objects.order_by('party_name')
except PartyTotalBillsView.DoesNotExist:
party_list = None
paginator = Paginator(party_list, 2) # Show 2 rows per page: for Test
page_number = request.GET.get('page')
party_list = paginator.get_page(page_number)
return render(request, 'mael/parties.html', {'party_list': party_list})
Файл шаблона
<form id="search-form" method="get" action="/mael/parties/">
<input id="search-text" type="text" name="q" placeholder="Enter search keyword">
<input class="btn-search-party" type="submit" value="Search" />
</form>
<br/>
<table class="show-data">
<thead>
<tr>
<th>ID</th>
<th>Party Name</th>
<th>Total Bill Amount</th>
<th>Phone</th>
<th>Address</th>
<th></th>
</tr>
</thead>
{% if party_list %}
<tbody>
{% for party in party_list %}
<tr>
<td class="party-id">{{ party.party_id }}</td>
<td class="party-name">{{ party.party_name }}</td>
<td>{{ party.total_bills }}</td>
<td class="party-phone">{{ party.party_phone }}</td>
<td class="party-address">{{ party.party_address }}</td>
<td>
<button class="btn-modify" data-partyid="{{party.party_id}}" type="buttton">
Modify
</button>
</td>
</tr>
{% endfor %}
</tbody>
{% endif %}
</table>
<div class="pagination">
<span class="step-links">
{% if party_list.has_previous %}
<a href="?page=1">« first</a>
<a href="?page={{ party_list.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ party_list.number }} of {{ party_list.paginator.num_pages }}
</span>
{% if party_list.has_next %}
<a href="?page={{ party_list.next_page_number }}">next</a>
<a href="?page={{ party_list.paginator.num_pages }}">last »</a>
{% endif %}
</span>
</div>
Пожалуйста, не используйте два представления. ListView может также выполнять фильтрацию:
class PartyListView(ListView):
paginate_by = 2
model = PartyTotalBillsView
template_name = 'mael/parties.html'
context_object_name = 'party_list'
def querystring(self):
qs = self.request.GET.copy()
qs.pop(self.page_kwarg, None)
return qs.urlencode()
def get_queryset(self):
qs = super().get_queryset()
if 'q' in self.request.GET:
qs = qs.filter(party_name__icontains=self.request.GET['q'])
return qs.order_by('party_name')
В ссылках для предыдущей и следующей страниц вы добавляете querystring вида:
<span class="step-links">
{% if party_list.has_previous %}
<a href="?page=1&{{ view.querystring }}">« first</a>
<a href="?page={{ page_obj.previous_page_number }}&{{ view.querystring }}">previous</a>
{% endif %}
<span class="current">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
{% if party_list.has_next %}
<a href="?page={{ page_obj.next_page_number }}&{{ view.querystring }}">next</a>
<a href="?page={{ page_obj.paginator.num_pages }}&{{ view.querystring }}">last »</a>
{% endif %}
</span>
Pagination & CBV
Если вы используете django generic ListView с атрибутом paginate_by, вам не нужно создавать экземпляр paginator. Либо вы используете CBV (Class Based View) или Function Views, но не оба варианта.
Для отображения HTML создайте страницу _django_pager.html для включения в страницы списка.
{% comment %}
https://getbootstrap.com/docs/4.1/components/pagination/
{% endcomment %}
{% if is_paginated %}
<ul class="pagination">
{% if page_obj.has_previous %}
<li class="page-item"><a class="page-link" href="?{% url_replace page=page_obj.previous_page_number %}">«</a></li>
{% else %}
<li class="page-item disabled"><a href="#" class="page-link">«</a></li>
{% endif %}
{% for i in page_obj.paginator.page_range %}
{% if page_obj.number == i %}
<li class="page-item active"><a href="#" class="page-link">{{ i }}<span class="sr-only">(current)</span></a></li>
{% else %}
<li class="page-item"><a class="page-link" href="?{% url_replace page=i %}">{{ i }}</a></li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item"><a class="page-link" href="?{% url_replace page=page_obj.next_page_number %}">»</a></li>
{% else %}
<li class="page-item disabled"><a href="#" class="page-link">»</a></li>
{% endif %}
</ul>
{% endif %}
Фильтрация
Q объект является мощным для построения сложных запросов, но вы должны указать поле БД, к которому применяется Q объект.
Вместо жесткого кодирования формы в HTML я рекомендую использовать класс формы. Поэтому в forms.py создайте PartySearchForm
class PartySearchForm(forms.Form):
"""
Search in party
"""
search_text = forms.CharField(max_length=100,
required=False,
widget=forms.TextInput(attrs={
"class": "form-control",
"placeholder": "Search"
})
)
Вариант 1: фильтровать набор запросов в представлении
class PartyListView(ListView):
model = PartyTotalBillsView
form = PartySearchForm
paginate_by = 100
def build_where(self):
where = Q(pk__gt=0)
if self.request.GET.get("search_text"):
search_list = self.request.GET.get("search_text", None).split()
for search_item in search_list:
where &= (
Q(party_name__icontains=search_item)
)
return where
def get_queryset(self):
qs = self.model.objects.all()
qswhere = qs.filter(self.build_where())
# first param must be request.GET or None (essential for the first load and initial values)
# https://www.peterbe.com/plog/initial-values-bound-django-form-rendered
self.form = PartySearchForm(self.request.GET or None)
return qswhere
В функции build_where вы можете добавить столько полей поиска, сколько хотите. Вы можете искать по другим полям БД, кроме party_name, добавляя поля в переменную where.
where &= (
Q(party_name__icontains=search_item)
| Q(party_location__icontains=search_item)
)
Вы также можете добавить в форму другие поля поиска, кроме search_text, и добавить Q-поиск по переменной where.
if self.request.GET.get("my_new_field"):
where &= Q(supplier=self.request.GET.get("my_new_field", ""))
Ключевым моментом здесь является метод get_queryset, в котором определяется отображаемый набор запросов, то есть: выборка, фильтрация и сортировка (который также может быть методом). .order_by('party_name') не пригодится, если вы добавите класс Meta в models.py
class Meta:
verbose_name = "Let's go party"
ordering = ['party_name']
Другим способом может быть передача queryset в форму и выполнение поиска
Вариант 2: фильтровать набор запросов в форме
Выглядит еще чище с логикой поиска только в SearchForm!
PartyListView.get_queryset стать
def get_queryset(self):
qs1 = self.model.objects.all()
self.form = PartySearchForm(self.request.GET, queryset=qs1)
qs = self.form.get_queryset(self.request.GET)
return qs
PartySearchForm стать
class PartySearchForm(forms.Form):
"""
Search in party
"""
search_text = forms.CharField(max_length=100,
required=False,
widget=forms.TextInput(attrs={
"class": "form-control",
"placeholder": "Search"
})
)
def __init__(self, *args, **kwargs):
"""
Takes an option named argument ``queryset`` as the base queryset used in
the ``get_queryset`` method.
"""
self.queryset = kwargs.pop("queryset", None)
super().__init__(*args, **kwargs)
def get_queryset(self, request):
where = Q(pk__gt=0)
# is_valid() check is important to get access to cleaned_data
if not self.is_valid():
return self.queryset
search_text = self.cleaned_data.get("search_text").strip()
if search_text:
search_list = search_text.split()
for search_item in search_list:
where &= (
Q(party_name__icontains=search_item)
)
qs = self.queryset.filter(where)
return qs.distinct()
В конечном итоге, если вы используете Postgres DB и хотите углубиться в текстовый поиск, вы можете реализовать Django полнотекстовый поиск. Плюсы и минусы можно узнать, прочитав это.