Enhancing Django ListView with Dynamic Filtering: A Step-by-Step Guide

Filtering a List View Based on Fields in Django
December 3, 2024 by
Enhancing Django ListView with Dynamic Filtering: A Step-by-Step Guide
Hamed Mohammadi
| No comments yet

Filtering data is a core requirement in many web applications, especially in real estate, e-commerce, or any platform with large datasets. In this post, we’ll explore how to enhance Django’s ListView with dynamic filtering options for properties, allowing users to filter based on attributes such as location, property type, listing type, and price range. This tutorial assumes a basic understanding of Django and its class-based views.

The Scenario: A Property Listing Page

We have a Property model representing real estate listings. The goal is to allow users to filter the properties based on:

  • Country, State, and City: To narrow results geographically.
  • Property Type: Apartment, House, Land, etc.
  • Listing Type: For Sale or For Rent.
  • Price Range: Minimum and maximum price.

Here’s the simplified Property model:

from django.db import models
from djmoney.models.fields import MoneyField
import uuid

class Property(models.Model):
    class PropertyType(models.TextChoices):
        APARTMENT = "AP", "Apartment"
        HOUSE = "HS", "House"
        LAND = "LD", "Land"
        OTHER = "OT", "Other"

    class ListingType(models.TextChoices):
        FOR_RENT = "RE", "For Rent"
        FOR_SALE = "SA", "For Sale"

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    title = models.CharField(max_length=255)
    country = models.CharField(max_length=100)
    state = models.CharField(max_length=100)
    city = models.CharField(max_length=100)
    property_type = models.CharField(
        max_length=2, choices=PropertyType.choices, default=PropertyType.OTHER
    )
    listing_type = models.CharField(
        max_length=2, choices=ListingType.choices, default=ListingType.FOR_SALE
    )
    price = MoneyField(max_digits=14, decimal_places=2, default_currency="USD", default=0.0)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

Step 1: Extending ListView for Filtering

Django’s ListView provides a great starting point for displaying lists of objects. To add filtering, override the get_queryset method:

from django.views.generic import ListView

class PropertyListView(ListView):
    model = Property
    context_object_name = "properties"
    paginate_by = 10  # Optional: Paginate the property list

    def get_queryset(self):
        queryset = super().get_queryset()

        # Retrieve filters from request
        country = self.request.GET.get("country")
        state = self.request.GET.get("state")
        city = self.request.GET.get("city")
        property_type = self.request.GET.get("property_type")
        listing_type = self.request.GET.get("listing_type")
        min_price = self.request.GET.get("min_price")
        max_price = self.request.GET.get("max_price")

        # Apply filters
        if country:
            queryset = queryset.filter(country__iexact=country)
        if state:
            queryset = queryset.filter(state__iexact=state)
        if city:
            queryset = queryset.filter(city__iexact=city)
        if property_type:
            queryset = queryset.filter(property_type=property_type)
        if listing_type:
            queryset = queryset.filter(listing_type=listing_type)
        if min_price:
            queryset = queryset.filter(price__gte=min_price)
        if max_price:
            queryset = queryset.filter(price__lte=max_price)

        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["filters"] = {
            "country": self.request.GET.get("country", ""),
            "state": self.request.GET.get("state", ""),
            "city": self.request.GET.get("city", ""),
            "property_type": self.request.GET.get("property_type", ""),
            "listing_type": self.request.GET.get("listing_type", ""),
            "min_price": self.request.GET.get("min_price", ""),
            "max_price": self.request.GET.get("max_price", ""),
        }
        context["property_type_choices"] = Property.PropertyType.choices
        context["listing_type_choices"] = Property.ListingType.choices
        return context

            

Step 2: Creating a Filter Form in the Template

We’ll build a filter form in property_list.html that submits filter criteria via GET:

<form method="get">
    <div>
        <label for="country">Country:</label>
        <input type="text" name="country" id="country" value="{{ filters.country }}">
    </div>
    <div>
        <label for="state">State:</label>
        <input type="text" name="state" id="state" value="{{ filters.state }}">
    </div>
    <div>
        <label for="city">City:</label>
        <input type="text" name="city" id="city" value="{{ filters.city }}">
    </div>
    <div>
        <label for="property_type">Property Type:</label>
        <select name="property_type" id="property_type">
            <option value="">-- Select --</option>
            {% for key, value in property_type_choices %}
                <option value="{{ key }}" {% if filters.property_type == key %}selected{% endif %}>
                    {{ value }}
                </option>
            {% endfor %}
        </select>
    </div>
    <div>
        <label for="listing_type">Listing Type:</label>
        <select name="listing_type" id="listing_type">
            <option value="">-- Select --</option>
            {% for key, value in listing_type_choices %}
                <option value="{{ key }}" {% if filters.listing_type == key %}selected{% endif %}>
                    {{ value }}
                </option>
            {% endfor %}
        </select>
    </div>
    <div>
        <label for="min_price">Min Price:</label>
        <input type="number" name="min_price" id="min_price" value="{{ filters.min_price }}">
    </div>
    <div>
        <label for="max_price">Max Price:</label>
        <input type="number" name="max_price" id="max_price" value="{{ filters.max_price }}">
    </div>
    <button type="submit">Filter</button>
</form>

<ul>
    {% for property in properties %}
        <li>
            <a href="{{ property.get_absolute_url }}">{{ property.title }}</a>
            <p>{{ property.city }}, {{ property.state }}, {{ property.country }}</p>
            <p>Price: {{ property.price }}</p>
        </li>
    {% endfor %}
</ul>

{% if is_paginated %}
    <div class="pagination">
        {% if page_obj.has_previous %}
            <a href="?{% if filters %}{{ filters|urlencode }}&{% endif %}page={{ page_obj.previous_page_number }}">Previous</a>
        {% endif %}
        <span>Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
        {% if page_obj.has_next %}
            <a href="?{% if filters %}{{ filters|urlencode }}&{% endif %}page={{ page_obj.next_page_number }}">Next</a>
        {% endif %}
    </div>
{% endif %}


Step 3: Testing and Extending

With this setup:

  1. Visit the property listing page.
  2. Use the filter form to refine results dynamically.
  3. Ensure pagination works correctly with filters.

You can further extend this by integrating Django forms, adding AJAX-based filtering, or caching results for performance.

Conclusion

Adding dynamic filters to Django ListView enhances the user experience by allowing targeted searches. By leveraging Django’s TextChoices, get_queryset, and context, we can build flexible and maintainable filtering systems suitable for a variety of applications.

Got any other tips or questions about Django? Share them in the comments below!

Enhancing Django ListView with Dynamic Filtering: A Step-by-Step Guide
Hamed Mohammadi December 3, 2024
Share this post
Tags
Archive

Please visit our blog at:

https://zehabsd.com/blog

A platform for Flash Stories:

https://readflashy.com

A platform for Persian Literature Lovers:

https://sarayesokhan.com

Sign in to leave a comment