didierCH
didierCH

Reputation: 428

How to exclude empty values in an AND queryset whitout writing if else statements

For a real-estate listing site I have a search form with multiple inputs. The user can use one or many fields to filter his search results. The more fields are filled in the more detailed the search should get.

For example: If you only input the zip code all listings that match the zip code should be shown, if you input the zip code and the room number only listings that match both should be shown. My forms.py looks like this:

from django import forms

from .models import Property

class PropertyForm(forms.Form):
    zip_code = forms.CharField(required=False)
    rooms = forms.CharField(required=False)

Because I need a AND search I had to write a series of conditions in my ListView to query the results:

My views.py looks like this:

class ListPageView(ListView):
    template_name = 'property_list.html'
    model = Property

    def get_queryset(self):
        plz = self.request.GET.get('plz')
        rooms = self.request.GET.get('rooms')
        if plz and not rooms:
            object_list = Property.objects.filter(zip_code__exact=plz)
        elif rooms and not plz:
            object_list = Property.objects.filter(rooms__exact=rooms)
        elif plz and rooms:
            object_list = Property.objects.filter(
                Q(zip_code__icontains=plz) & Q(rooms__exact=rooms)
            )
        else:
            object_list = self.model.objects.none()

        return object_list

For only two input fields this would suffice but if I add more input fields I quickly have to write a lot of queries. Is there a better way to make AND queries that automatically exclude empty values?

Upvotes: 0

Views: 86

Answers (2)

Azdy
Azdy

Reputation: 320

Use optional fields in And operation in django queryset by Q and reduce:

import operator  
from django.db.models import Q  
import functools  
class ListPageView(ListView):  
   template_name = 'property_list.html'  
   model = Property  
   def get_queryset(self):  
      fields = ["plz", "rooms"]  
      predicates = []  
      for field in fields:  
         key=field+"__exact"  
         value=self.request.GET.get(field)  
         if value:  
            predicates.append((key,value))  
     queryList = [Q(x) for x in predicates]  
     queryset = Property.objects.filter(functools.reduce(operator.and_, queryList))
    return queryset

Upvotes: 2

Daniel Hepper
Daniel Hepper

Reputation: 29967

Instead of creating a queryset for each possible combination of parameters, you can start with an unfiltered queryset and add filters step-by-step:

class ListPageView(ListView):
    template_name = 'property_list.html'
    model = Property

    def get_queryset(self):
        plz = self.request.GET.get('plz')
        rooms = self.request.GET.get('rooms')
        if any([plz, rooms]):
            object_list = Property.objects.all()
            if plz:
                object_list = object_list.filter(zip_code__exact=plz)
            if rooms:
                object_list = object_list.filter(rooms__exact=rooms)
        else:
            object_list = Property.objects.none()

        return object_list

To make this less repetitive, you could use a loop:

class ListPageView(ListView):
    template_name = 'property_list.html'
    model = Property

    def get_queryset(self):
        field_mapping = (  # mapping field names to parameter names
            ('zip_code', 'plz'),
            ('rooms', 'rooms'),
        )
        # Building a dictionary with the parameters, equal to:
        # params = {
        #     'zip_code': self.request.GET.get('plz'),
        #     'rooms': self.request.GET.get('rooms'),
        # }
        params = {
            field_name: self.request.GET.get(param_name)
            for field_name, param_name
            in field_mapping
        }
        if any(params.values()):
            object_list = Property.objects.all()
            for field_name, value in params.items():
                if value:
                    # Passing a dictionary with one dynamically
                    # built key as keyword arguments to filter
                    object_list = object_list.filter(
                        **{f"{field_name}__exact":value}
                    )
        else:
            object_list = Property.objects.none()

        return object_list

Upvotes: 2

Related Questions