How to handle sorting, filtering and pagination in the same ListView

GitHub link: https://github.com/IgorArnaut/Django-ListView-Pagination-Search-Sorting-Issue I have an issue with the ListView.

I have pagination, side form for filtering and a form with select for sorting in a single view. These 3 use get method and are "handled" in this list view.

class ListingListView(generic.ListView):
    model = Listing
    context_object_name = "listings"
    template_name = "listings/listing_list.html"

    def get_context_data(self, **kwargs):
        context = super(ListingListView, self).get_context_data(**kwargs)
        context["form"] = SearchForm()
        return context
    
    def get_queryset(self):
        queryset = super(ListingListView, self).get_queryset()

        if (self.request.GET.dict()):
            query = filter(self.request.GET.dict())
            queryset = queryset.filter(query)
        
        sorting = self.request.GET.get("sorting")

        if sorting == "new":
            queryset = queryset.order_by("-created_at")
        if sorting == "big":
            queryset = queryset.order_by("-apartment__area")
        if sorting == "small":
            queryset = queryset.order_by("apartment__area")
        if sorting == "expensive":
            queryset = queryset.order_by("-apartment__price")
        if sorting == "cheap":
            queryset = queryset.order_by("apartment__price")

        paginator = Paginator(queryset, per_page=2)
        page_number = self.request.GET.get("page")
        listings = paginator.get_page(page_number)
        return listings

Urls:

urlpatterns = [
    path("", views.ListingListView.as_view(), name="listing-list"),
    path("postavka", login_required(views.ListingCreateView.as_view()), name="listing-create"),
    path("<int:pk>", views.ListingDetailView.as_view(), name="listing-detail"),
    path("<int:pk>/izmena", views.ListingUpdateView.as_view(), name="listing-update"),
    path("<int:pk>/brisanje", login_required(views.listing_delete), name="listing-delete")
]

Templates:

<div class="col-4">{% include "../partials/listings/search_form.html" %}</div>
  <div class="col-6">
    <form class="mb-3" action="{% url 'listing-list' %}" method="get">
      <select class="form-select w-50" name="sorting">
        <option value="new">Prvo najnoviji</option>
        <option value="popular">Prvo najpopularniji</option>
        <option value="big">Po kvadraturi (opadajuće)</option>
        <option value="small">Po kvadraturi (rastuće)</option>
        <option value="expensive">Po ceni (opadajuće)</option>
        <option value="cheap">Po ceni (rastuće)</option>
      </select>
    </form>
    {% if listings %}
      {% for listing in listings %}
        {% include "../partials/listings/list_card.html" with listing=listing %}
      {% endfor %}
    {% else %}
       <div class="row">Oglasi nisu dostupni.</div>
    {% endif %}
    <ul class="pagination">
      {% if listings.has_previous %}
        <li class="page-item">
          <a class="page-link" href="?page=1">&laquo; first</a>
        </li>
        <li class="page-item">
          <a class="page-link" href="?page={{ listings.previous_page_number }}">previous</a>
        </li>
      {% endif %}
        <span class="current">Page {{ listings.number }} of {{ listings.paginator.num_pages }}.</span>
      {% if listings.has_next %}
        <li class="page-item">
          <a class="page-link" href="?page={{ listings.next_page_number }}">next</a>
        </li>
        <li class="page-item">
          <a class="page-link" href="?page={{ listings.paginator.num_pages }}">last &raquo;</a>
        </li>
      {% endif %}
    </ul>
<form class="mb-3" action="{% url 'listing-list' %}" method="get">
  {# {% csrf_token %} #}
    {% for field in search_form %}
      <div class="row mb-3">
        <label class="col-sm-4 col-form-label">{{ field.label }}</label>
        <div class="col-sm-8">{{ field }}</div>
      </div>
    {% endfor %}
  <input class="btn btn-primary fw-semibold" type="submit" value="Pretraži">
</form>

I have 3 listings in the database, but I show only 2 per page. When I go to the next page, I het an KeyError for city, a field which is not used in pagination, but in side form.

Traceback (most recent call last):
  File "D:\Documents\_Igor\Projekti\_Master\Real Estate Listings App\Secure\Django\venv\Lib\site-packages\django\core\handlers\exception.py", line 55, in inner
    response = get_response(request)
  File "D:\Documents\_Igor\Projekti\_Master\Real Estate Listings App\Secure\Django\venv\Lib\site-packages\django\core\handlers\base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "D:\Documents\_Igor\Projekti\_Master\Real Estate Listings App\Secure\Django\venv\Lib\site-packages\django\views\generic\base.py", line 105, in view
    return self.dispatch(request, *args, **kwargs)
           ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\Documents\_Igor\Projekti\_Master\Real Estate Listings App\Secure\Django\venv\Lib\site-packages\django\views\generic\base.py", line 144, in dispatch
    return handler(request, *args, **kwargs)
  File "D:\Documents\_Igor\Projekti\_Master\Real Estate Listings App\Secure\Django\venv\Lib\site-packages\django\views\generic\list.py", line 158, in get
    self.object_list = self.get_queryset()
                       ~~~~~~~~~~~~~~~~~^^
  File "D:\Documents\_Igor\Projekti\_Master\Real Estate Listings App\Secure\Django\realestate\listings\views.py", line 27, in get_queryset
    query = filter(self.request.GET.dict())
  File "D:\Documents\_Igor\Projekti\_Master\Real Estate Listings App\Secure\Django\realestate\listings\helpers\search.py", line 28, in filter
    query &= filter_by_city(data["city"])
                            ~~~~^^^^^^^^
KeyError: 'city'

You filter with:

def filter(data):
    query = Q()
    query &= filter_by_city(data["city"])
    query &= filter_by_num_of_rooms(data["num_of_rooms"])
    query &= filter_by_price(data["price_from"], data["price_to"])
    query &= filter_by_area(data["m2_from"], data["m2_to"])
    return query

But it is not said data contains all these keys. Your filter_by_city(..) and other functions contain checks for None, so we can use:

def filter(data):
    query = Q()
    query &= filter_by_city(data.get("city"))
    query &= filter_by_num_of_rooms(data.get("num_of_rooms"))
    query &= filter_by_price(data.get("price_from"), data.get("price_to"))
    query &= filter_by_area(data.get("m2_from"), data.get("m2_to"))
    return query

That being said, I would strongly advise to use a tool like django-filter, since this can automate a lot of this, like the range checks, and only filtering on aspects filled in.

Вернуться на верх