Home > other >  Use Q object and Paginator together in Django
Use Q object and Paginator together in Django

Time:02-16

I've created View which filters data by search query given in textbox. As well as I used Paginator to show data divided into pages.

My problem is, when I filter data with Q object then and try to paginate by clicking the next button, all data is refreshed.

When I search text by Q object the URL becomes http://127.0.0.1:8000/mael/parties/?q=keyword

And from clicking the next button the URL becomes http://127.0.0.1:8000/mael/parties/?page=2

When I manually change URL http://127.0.0.1:8000/mael/parties/?q=keyword&page=2, then it works. But I don't know how to do this in code.

Is it possible to use Q object search and pagination together?

My View

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})

Template file

<form id="search-form" method="get" action="/mael/parties/">
    <input id="search-text" type="text" name="q" placeholder="Enter search keyword">
    <input  type="submit" value="Search" />
</form>        
<br/>


<table >
    <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 >{{ party.party_id }}</td>
        <td >{{ party.party_name }}</td>
        <td>{{ party.total_bills }}</td>
        <td >{{ party.party_phone }}</td>
        <td >{{ party.party_address }}</td>
        <td>
            <button  data-partyid="{{party.party_id}}" type="buttton">
            Modify
            </button>
        </td>
        </tr>
    {% endfor %}
    </tbody>
    {% endif %}
</table>


<div >
    <span >
    {% if party_list.has_previous %}
        <a href="?page=1">&laquo; first</a>
        <a href="?page={{ party_list.previous_page_number }}">previous</a>
    {% endif %}

    <span >
        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 &raquo;</a>
    {% endif %}
    </span>
</div>

CodePudding user response:

Please do not use two views. A ListView can perform filtering as well:

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')

In the links for the previous and next pages, you then append the querystring of the view:

    <span >
    {% if party_list.has_previous %}
        <a href="?page=1&amp;{{ view.querystring }}">&laquo; first</a>
        <a href="?page={{ page_obj.previous_page_number }}&amp;{{ view.querystring }}">previous</a>
    {% endif %}

    <span >
        Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
    </span>

    {% if party_list.has_next %}
        <a href="?page={{ page_obj.next_page_number }}&amp;{{ view.querystring }}">next</a>
        <a href="?page={{ page_obj.paginator.num_pages }}&amp;{{ view.querystring }}">last &raquo;</a>
    {% endif %}
    </span>

CodePudding user response:

Pagination & CBV

If you are using django generic ListView with paginate_by attribute, you don't need to build paginator instance. Either you use CBV (Class Based View) or Function Views but not both.

For HTML display create a _django_pager.html page to include in your list pages.

{% comment %}
https://getbootstrap.com/docs/4.1/components/pagination/
{% endcomment %}

{% if is_paginated %}

  <ul >

    {% if page_obj.has_previous %}

      <li ><a  href="?{% url_replace page=page_obj.previous_page_number %}">&laquo;</a></li>
    {% else %}
      <li ><a href="#" >&laquo;</a></li>
    {% endif %}

    {% for i in page_obj.paginator.page_range %}
      {% if page_obj.number == i %}
        <li ><a href="#" >{{ i }}<span >(current)</span></a></li>
      {% else %}
        <li ><a  href="?{% url_replace page=i %}">{{ i }}</a></li>
      {% endif %}
    {% endfor %}


    {% if page_obj.has_next %}
      <li ><a  href="?{% url_replace page=page_obj.next_page_number %}">&raquo;</a></li>
    {% else %}
      <li ><a href="#" >&raquo;</a></li>
    {% endif %}
  </ul>

{% endif %}

Filtering

Q object is powerfull for building complex query but you have to specify DB field under which Q object applies.

Instead of hardcoding the form in HTML I recommand to use a Form Class. So in forms.py create a 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"
                              })
                          )

Option 1: filter queryset in view

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

In the build_where function you can add as many search field as you want. You can search on other DB field than party_name by adding the fields to the where variable.

where &= (
    Q(party_name__icontains=search_item)
    | Q(party_location__icontains=search_item)
)

You can also add other search fields than search_text in your form and add Q search on the where variable.

if self.request.GET.get("my_new_field"):
   where &= Q(supplier=self.request.GET.get("my_new_field", ""))

Key point here is the get_queryset method where the displayed queryset is defined, ie: fetched, filtered and sorted (which could also be a method). .order_by('party_name') is not useful if you add a class Meta in models.py

    class Meta:
        verbose_name = "Let's go party"
        ordering = ['party_name']

One other way to do would be to pass the queryset to the form and perform the search

Option 2: filter queryset in form

Looks even cleaner with the search logic in the SearchForm only!

PartyListView.get_queryset become

    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 become

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()

Eventually, if you are using Postgres DB and want to go deeper with Text Search you can implement Django full text search. Pros & cons can be gained by reading this.

  • Related