You have a dozen of views for listing Model objects. One day your client wants to
add search into every page which are listing objects. Nightmare!! I had a couple of views like this
which lists the objects in a table. I was happy with what provided by the
django’s generic ListView
. I just had to inherit from it and define the model and
template. So i didn’t want to give away this comfort. I just wanted the the same function,
with search enabled on certain fields defined. This is where we can make use of the real
power of django’s class based views. There is a method get_queryset
in
ListView
which returns the list of all objects in the given model. By
overriding this method, instead of returning all objects we can do the
filtering based on the search term passed as get parameter and return the subset.
The rest will work as it is :)
from django.views.generic.list import ListView
class ListSearchView(ListView):
"""
This is a view that can be inherited from to add search functionality to a
ListView.
An extra context variable `query_string` will be available in template.
This can be used to print the search string.
"""
order_by_field = None
search_fields = []
def get_order_by_field(self):
"""
Get the field by which the results will be ordered.
If `order_by_field` is not defined no ordering will be done.
"""
return self.order_by_field
def get_search_fields(self):
"""
Return the fields in table to search for.
"""
return self.search_fields
def get_queryset(self):
"""
Get the query string from url and find the items from the database.
"""
self.query_string = ''
found_items = None
queryset = super(ListSearchView, self).get_queryset()
# Check if query string is there in get, otherwise just return the
# queryset.
if ('q' in self.request.GET) and self.request.GET['q'].strip():
self.query_string = self.request.GET['q']
# Q object made by ORing search fields
entry_query = get_query(self.query_string, self.get_search_fields())
found_items = queryset.filter(entry_query)
# Order if order_by field is given
order_by_field = self.get_order_by_field()
if order_by_field:
found_items = found_items.order_by('%s' % order_by_field)
return found_items
else:
return queryset
def get_context_data(self, **kwargs):
"""
Update the context data with query_string.
"""
kwargs.update({'query_string': self.query_string})
return super(ListSearchView, self).get_context_data(**kwargs)
This view depends on the get_query
method by Julien Phalip.
On providing the search term and the fields to search it will return a
combination of Q objects which can be passed to Queryset’s filter method for
filtering it.
import re
from django.db.models import Q
def normalize_query(query_string,
findterms=re.compile(r'"([^"]+)"|(\S+)').findall,
normspace=re.compile(r'\s{2,}').sub):
"""
Splits the query string in invidual keywords, getting rid of unecessary spaces
and grouping quoted words together.
Example:
>>> normalize_query(' some random words "with quotes " and spaces')
['some', 'random', 'words', 'with quotes', 'and', 'spaces']
"""
return [normspace(' ', (t[0] or t[1]).strip()) for t in findterms(query_string)]
def get_query(query_string, search_fields):
"""
Returns a query, that is a combination of Q objects. That combination
aims to search keywords within a model by testing the given search fields.
"""
query = None # Query to search for every search term
terms = normalize_query(query_string)
for term in terms:
or_query = None # Query to search for a given term in each field
for field_name in search_fields:
q = Q(**{"%s__icontains" % field_name: term})
if or_query is None:
or_query = q
else:
or_query = or_query | q
if query is None:
query = or_query
else:
query = query & or_query
return query
Now you can extend ListSearchView
instead of generic ListView
to support search in
your views like this.
class ContactListView(ListSearchView):
model = Contact
template_name = 'list_contacts.html'
search_fields = ['first_name', 'last_name']
ListSearchView
also injects the search term into the context data. So in
template you can check if query_string
exists and add messages like the
following.
{% if query_string %}
Search results for <strong>{{ query_string }}</strong> in My Contacts
{% endif %}
You can add search box like the following, So when the user is searching, the search term will be filled in the textbox, If there is no search term it will show the placeholder ‘Search’.
<form class="navbar-search pull-left" method="GET" action="">
<input type="text" value="{{ query_string }}" placeholder="Search" name="q">
</form>
The same can be achieved by constructing a Mixin from this and using it with
the ListView
in your existing views. But I prefer overriding ListView
and
using ListSearchView
.
That’s all for now! Happy hacking with django :-)