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:
- Visit the property listing page.
- Use the filter form to refine results dynamically.
- 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!