Search and Filter

Towel does not distinguish between searching and filtering. There are different layers of filtering applied during a request and depending on your need you have to hook in your filter at the right place.

Making lists searchable using the search form

Pagination is not enough for many use cases, we need more! Luckily, Towel has a pre-made solution for searching object lists too.

towel.forms.SearchForm can be used together with towel.managers.SearchManager to build a low-cost implementation of full text search and filtering by model attributes.

The method used to implement full text search is a bit stupid and cannot replace mature full text search solutions such as Apache Solr. It might just solve 80% of the problems with 20% of the effort though.

Code talks. First, we extend our models definition with a Manager subclass with a simple search implementation:

from django.db import models
from towel.managers import SearchManager

class BookManager(SearchManager):
    search_fields = ('title', 'topic', 'authors__name',
        'publisher__name', 'publisher__address')

class Book(models.Model):
    # [...]

    objects = BookManager()

SearchManager supports queries with multiple clauses; terms may be grouped using apostrophes, plus and minus signs may be optionally prepended to the terms to determine whether the given term should be included or not. Example:

+Django "Shop software" -Satchmo

Please note that you can search fields from other models too. You should be careful when traversing many-to-many or reverse foreign key relations however, because you will get duplicated results if you do not call distinct() on the resulting queryset.

The method _search() does the heavy lifting when constructing a queryset. You should not need to override this method. If you want to customize the results further, f.e. apply a site-wide limit for the objects a certain logged in user may see, you should override search().

Next, we have to create a SearchForm subclass:

from django import forms
from towel import forms as towel_forms
from myapp.models import Author, Book, Publisher

class BookSearchForm(towel_forms.SearchForm):
    publisher = forms.ModelChoiceField(Publisher.objects.all(), required=False)
    authors = forms.ModelMultipleChoiceField(Author.objects.all(), required=False)
    published_on__lte = forms.DateField(required=False)
    published_on__gte = forms.DateField(required=False)

    formfield_callback = towel_forms.towel_formfield_callback

You have to add required=False to every field if you do not want validation errors on the first visit to the form (which would not make a lot of sense, but isn’t actively harmful).

As long as you only use search form fields whose names correspond to the keywords used in Django’s .filter() calls or Q() objects you do not have to do anything else.

The formfield_callback simply substitutes a few fields with whitespace-stripping equivalents, and adds CSS classes to DateInput and DateTimeInput so that they can be easily augmented by javascript code.

Warning

If you want to be able to filter by multiple items, i.e. publishers 1 and 2, you have to define the publisher field in the SearchForm as ModelMultipleChoiceField. Even if the model itself only has a simple ForeignKey Field. Otherwise only the last element of a series is used for filtering.

To activate a search form, all you have to do is add an additional parameter when you instantiate a ModelView subclass:

from myapp.forms import BookSearchForm
from myapp.models import Book
from towel.modelview import ModelView

urlpatterns = patterns('',
    url(r'^books/', include(ModelView(Book,
        search_form=BookSearchForm,
        paginate_by=20,
        ).urls)),
)

You can now filter the list by providing the search keys as GET parameters:

localhost:8000/books/?author=2
localhost:8000/books/?publisher=4&o=authors
localhost:8000/books/?authors=4&authors=5&authors=6

Advanced SearchForm features

The SearchForm has a post_init method, which receives the request and is useful if you have to further modify the queryset i.e. depending on the current user:

def post_init(self, request):
    self.access = getattr(request.user, 'access', None)
    self.fields['publisher'].queryset = Publisher.objects.for_user(request.user)

The ordering is also defined in the SearchForm. You have to specify a dict called orderings which has the ordering key as first parameter. The second parameter can be a field name, an iterable of field names or a callable. The ordering keys are what is used in the URL:

class AddressSearchForm(SearchForm):
    orderings = {
        '': ('last_name', 'first_name'),  # Default
        'dob': 'dob',  # Sort by date of birth
        'random': lambda queryset: queryset.order_by('?'),
        }

Persistent queries

When you pass the parameter s, the search is stored in the session for that path. If the user returns to the object list, the filtering is applied again.

The field is included in the SearchForm by default, but don’t forget to add it to your template if you are using a custom form render method.

To reset the filters, you have to pass ?clear=1 or ?n.

Quick Rules

Another option for filtering are Quick rules. This allows for field-independent filtering like is:cool. Quick rules are mapped to filter attributes using regular expressions. They go into the search form and are parsed automatically (as long as query_data is used inside the queryset method:

class BookSearchForm(towel_forms.SearchForm):
    quick_rules = [
        (re.compile(r'has:publisher'), quick.static(publisher__isnull=False)),
        (re.compile(r'is:published'), quick.static(published_on__lt=timezone.now)),
    ]