user3138929
user3138929

Reputation: 389

django model search form

Firstly, I did my homework and looked around before posting! My question seems like a very basic thing that must’ve been covered before.

I'm now looking at Django-filter as a potential solution, but would like some advice on if this is the right way to go and if there any other solutions.

I have a Django app wit 10 models, each model has a few fields. Most fields are ChoiceField that users populate using forms with the default select widget. There is a separate form for each model.

I want to create a separate form for each model (in separate views) that users will use to search the database. The search form will contain only drop-down boxes (the select widgets) with the same choices as the forms used to populate the database with the addition of the “any” option.

I know how to use .object.filter(), however the “any” option would correspond to not include specific fields in the filter and I'm not sure how to add model fields to the filter based on users’ selection

I briefly looked at Haystack as an option but it seems to be made for full text search rather than “model filed search” I'm after.

Sample model (simplified):

class Property():             
      TYPE_CHOICES = (‘apartment’, ‘house’, ‘flat’)        
      type = charfield(choices=TYPE_CHOICES)
      LOC_CHOICES = (‘Brussels’, ‘London’, ‘Dublin’, ‘Paris’)
      location = charfield(choices=LOC_CHOICES)
      price = PostivieInteger()

Users can select only “type”, only “location” or both (not making selection is equal to ANY) in which case I end up with 3 different filters:

Property.objects.filter(type=’apartment’)
Property.objects.filter(location=’Dublin’)
Property.objects.filter(type=’apartment’, location=’Dublin’)

The main question: django-filter the best option?

Question 1: what’s the best option of accomplishing this overall? 
Question 2: how do I add model fields to the filter based on user’s form selection?
Question 3: how do I do the filter based on user selection? (I know how to use .filter(price_lt=).exclude(price_gt=) but again how do I do it dynamically based on selection as “ANY” would mean this is not included in the query)

Upvotes: 5

Views: 6953

Answers (2)

user3138929
user3138929

Reputation: 389

Your solution works. I've modified it and I'm not using ModelChoiceField but the standard form.ChoiceField. The reason for that is that I wanted to add option "Any". My "if" statements look like:

 if locality != 'Any Locality':
    qs = qs.filter(locality=locality)
if property_type != 'Any Type':
    qs = qs.filter(property_type=property_type)
if int(price_min) != 0:
    qs = qs.filter(price__gte=price_min)
if int(price_max) != 0:
    qs = qs.filter(price__lte=price_max)   
if bedrooms != 'Any Number':
    qs = qs.filter(bedrooms=bedrooms)

And so on....

This does the job, however it seems like an ugly and hacky solution to a simple problem. I would think is a common use case. I feel there should be a cleaner solution...

I've tried the django-filter. It is close to doing what I want but I couldn't add the "Any" choice and it filters inline rather than returning. It should do with some modifications.

Cheers

Upvotes: 2

petkostas
petkostas

Reputation: 7450

I had a similar case like yours (real estate project), I ended up with the following approach, you can refine this to your needs...I removed select_related and prefetch_related models for easier reading

properties/forms.py:

class SearchPropertyForm(forms.Form):

    property_type = forms.ModelChoiceField(label=_("Property Type"), queryset=HouseType.objects.all(),widget=forms.Select(attrs={'class':'form-control input-sm'}))
    location = forms.ModelChoiceField(label=_('Location'), queryset=HouseLocation.objects.all(), widget=forms.Select(attrs={'class':'form-control input-sm'}))

Then in the properties/views.py

# Create a Mixin to inject the search form in our context 

class SeachPropertyMixin(object):
    def get_context_data(self, **kwargs):
        context = super(SeachPropertyMixin, self).get_context_data(**kwargs)
        context['search_property_form'] = SearchPropertyForm()
        return context

In your actual view (I apply the search form as a sidebar element in my detailview only:

# Use Class Based views, saves you a great deal of repeating code...
class PropertyView(SeachPropertyMixin,DetailView):
    template_name = 'properties/view.html'
    context_object_name = 'house'
    ...
    queryset = HouseModel.objects.select_related(...).prefetch_related(...).filter(flag_active=True, flag_status='a')

Finally your search result view (this is performed as GET request, since we are not altering any data in our DB, we stick to the GET method):

# Search results should return a ListView, here is how we implement it:
class PropertySearchResultView(ListView):
    template_name = "properties/propertysearchresults.html"
    context_object_name = 'houses'
    paginate_by = 6
    queryset = HouseModel.objects.select_related(...).prefetch_related(...).order_by('-sale_price').filter(flag_active=True, flag_status='a')

    def get_queryset(self):
        qs = super(PropertySearchResultView,self).get_queryset()
        property_type = self.request.GET.get('property_type')
        location = self.request.GET.get('location')
        '''
        Start Chaining the filters based on the input, this way if the user has not 
        selected a filter it wont be used.
        '''
        if property_type != '' and property_type is not None:
            qs = qs.filter(housetype=property_type)
        if location != '' and location is not None:
            qs = qs.filter(location=location)
        return qs

    def get_context_data(self, **kwargs):
        context = super(PropertySearchResultView, self).get_context_data()
        ''' 
        Add the current request to the context 
        '''
        context['current_request'] = self.request.META['QUERY_STRING']
        return context

Upvotes: 5

Related Questions