Reputation: 73
I am working on cloning airbnb room registration system in Django. I have a class based view (HTTP get method) that filters rooms stored in the database according to various query string key value options and returns those rooms. Filter options provided through query string are:
location = request.GET.get('location')
adults = int(request.GET.get('adults', 0))
children = int(request.GET.get('children', 0))
infants = request.GET.get('infants', 0)
min_cost = float(request.GET.get('min_cost', 0))
max_cost = float(request.GET.get('max_cost', sys.maxsize))
property_type = request.GET.get('property_type', None)
place_type = request.GET.get('place_type', None)
check_in = request.GET.get('checkin', None)
check_in_date = datetime.datetime.strptime(check_in, '%Y-%m-%d') if check_in else None
check_out = request.GET.get('checkout', None)
check_out_date = datetime.datetime.strptime(check_out, '%Y-%m-%d') if check_out else None
min_beds = request.GET.get('min_beds', None)
min_bedrooms = request.GET.get('min_bedrooms', None)
min_baths = request.GET.get('min_baths', None)
amenities = request.GET.getlist('amenities', None)
languages = request.GET.getlist('languages', None)
I decided to store all filter expressions as Q() objects using &= operation. Rooms with already booked dates and host-chosen unavailable dates ('blockeddate') that are in conflict with provided check_in_date and check_out_date will be filtered out. After storing all Q() expression in a variable called 'queries', I passed 'queries' as an argument to Room.objects.filter() function. 'min_beds', 'min_bedrooms', and 'min_baths' options were evaluated after initial filtering so that I could perform annotate() function on the filtered queryset.
Following code works, but I am wondering if there is a more concise and efficient way of filtering in terms of db-calls and time complexity. Perhaps using prefetch_related()? As of now, there seems to be too many repetitive if statements, but I wasn't able to think of a better way of evaluating None cases for query string options.
queries = (
Q(address__icontains = location) &
Q(max_capacity__gte = adults + children) &
Q(price__range = (min_cost, max_cost))
)
if check_in_date and check_out_date:
queries &= (
~Q(blockeddate__start_date__range = (check_in_date, check_out_date)) &
~Q(blockeddate__end_date__range = (check_in_date, check_out_date)) &
~Q(booking__start_date__range = (check_in_date, check_out_date)) &
~Q(booking__end_date__range = (check_in_date, check_out_date))
)
if property_type:
queries &= Q(property_type__name = property_type)
if place_type:
queries &= Q(place_type__name = place_type)
if amenities:
q_expressions = [Q(amenities__name = amenity) for amenity in amenities]
for expression in q_expressions:
queries &= expression
if languages:
q_expressions = [Q(host__userlanguage__language__name = language) for language in languages]
for expression in q_expressions:
queries &= expression
room_qs = Room.objects.filter(queries)
if min_beds:
room_qs = room_qs.annotate(num_beds=Sum('bedroom__bed__quantity')).filter(num_beds__gte = min_beds)
if min_bedrooms:
room_qs = room_qs.annotate(num_bedrooms=Count('bedroom')).filter(num_bedrooms__gte = min_bedrooms)
if min_baths:
room_qs = room_qs.annotate(num_baths=Count('bath')).filter(num_baths__gte = min_baths)
Upvotes: 2
Views: 194
Reputation: 169075
Honestly, assuming indexes are in place not that I can see without maybe adding cache tables and such.
Once you have the queryset, you can print(queryset.query)
to get the SQL and plonk the result in EXPLAIN ...
. (The Django Debug Toolbar plugin can also do this.)
You can maybe DRY out the code a little:
from django.db.models import Q
def list_q(queries, field, values):
for value in values:
queries &= Q(**{field: value})
return queries
def annotation_filter(qs, name, aggregation, op, value):
if value:
return qs.annotate(**{name: aggregation}).filter(
**{f"{name}__{op}": value}
)
return qs
def q(...):
queries = (
Q(address__icontains=location)
& Q(max_capacity__gte=adults + children)
& Q(price__range=(min_cost, max_cost))
)
if check_in_date and check_out_date:
date_range = (check_in_date, check_out_date)
queries &= (
~Q(blockeddate__start_date__range=date_range)
& ~Q(blockeddate__end_date__range=date_range)
& ~Q(booking__start_date__range=date_range)
& ~Q(booking__end_date__range=date_range)
)
if property_type:
queries &= Q(property_type__name=property_type)
if place_type:
queries &= Q(place_type__name=place_type)
queries = list_q(queries, "amenities__name", amenities)
queries = list_q(
queries, "host__userlanguage__language__name", languages
)
room_qs = Room.objects.filter(queries)
room_qs = annotation_filter(
room_qs, "num_beds", Sum("bedroom__bed__quantity"), "gte", min_beds,
)
room_qs = annotation_filter(
room_qs, "num_bedrooms", Count("bedroom"), "gte", min_bedrooms,
)
room_qs = annotation_filter(
room_qs, "num_baths", Count("bath"), "gte", min_baths
)
return room_qs
Upvotes: 1
Reputation: 109
Check out the django-filters
package:
django-filters
It provides FilterSet
classes that encompass all the logic of filtering by multiple fields in a declarative way.
Upvotes: 2