Filter View in Django

Check out the new site at https://rkblog.dev.

In this tutorial we will make a filter view with a form for filtering data from columns, and (so it won't be a simple POST) we will use generic view with pagination. You may check screen shots at the end of the article.

As an example I'll use my Baldurs Gate Characters Catalogue - it has may fields which can be used for filtering:
class Character(models.Model):
	chr_file = models.FileField(upload_to='bgate')
	name = models.CharField(maxlength=100)
	xp = models.PositiveIntegerField()
	hp = models.PositiveIntegerField()
	avatar = models.CharField(maxlength=100)
	strength = models.PositiveSmallIntegerField()
	inteligence = models.PositiveSmallIntegerField()
	wisdom = models.PositiveSmallIntegerField()
	charisma = models.PositiveSmallIntegerField()
	constitution = models.PositiveSmallIntegerField()
	dex = models.PositiveSmallIntegerField()
	race = models.CharField(maxlength=50)
	main_class = models.CharField(maxlength=50)
	gender = models.CharField(maxlength=50)
	attacks = models.PositiveSmallIntegerField()
	death_save = models.PositiveSmallIntegerField()
	wands_save = models.PositiveSmallIntegerField()
	polymorph_save = models.PositiveSmallIntegerField()
	breath_save = models.PositiveSmallIntegerField()
	spells_save = models.PositiveSmallIntegerField()
	fire = models.PositiveIntegerField()
	cold = models.PositiveIntegerField()
	elect = models.PositiveIntegerField()
	acid = models.PositiveIntegerField()
	magic = models.PositiveIntegerField()
	magic_fire = models.PositiveIntegerField()
	magic_cold = models.PositiveIntegerField()
	slash = models.PositiveIntegerField()
	pierce = models.PositiveIntegerField()
	blunt = models.PositiveIntegerField()
	missile = models.PositiveIntegerField()
	thaco = models.IntegerField()
	levels = models.PositiveIntegerField()
	kit = models.CharField(maxlength=255)
	alingment = models.CharField(maxlength=255)
	author = models.ForeignKey(User)
	comment = models.TextField(maxlength=500)
	class Meta:
		verbose_name = _('Baldurs Gate Character')
		verbose_name_plural = _('Baldurs Gate Characters')
		#db_table = 'rk_baldur' + str(settings.SITE_ID)
		db_table = 'rk_baldur5'
	def get_absolute_url(self):
		return '/bgate/show/' + str(self.id) + '/'
	class Admin:
		list_display = ('name', 'author')
		search_fields = ['name', 'author']
	def __str__(self):
		return self.name
We start with a extended generic view:
def chr_filter(request, pagination_id=1):
	from django.views.generic.list_detail import object_list
	return object_list(request, Character.objects.all().order_by('-id'), paginate_by = 15, allow_empty = True, page = pagination_id, template_name = 'baldur/chr_filter.html')
The main part of our template looks like this:
<h2 class="pageh">{% trans "Baldurs Gate Characters" %}</h2>
<div class="content"><table><tr>
{% for t in object_list %}
<td><a href="/bgate/show/{{ t.id }}/" style="text-decoration:none;">{% if t.avatar %}<img src="/site_media/bgate_av/{{ t.avatar }}.jpg" alt="avatar" border="0" /><br />{% endif %}<strong>{{ t.name }}</strong><br />{{ t.main_class}} ({{ t.levels }} {% trans "level" %})</a>
{% cycle </td>,</td>,</td>,</td>,</td></tr><tr> %}
{% endfor %}
</tr></table></div>
<div class="box" style="text-align:center;">
{% if has_previous %}
<h3><a href="/bgate/list/{{ previous }}/">{% trans "Previous Page" %}</a></h3>
{% endif %}
{% if has_next %}
<h3><a href="/bgate/list/{{ next }}/">{% trans "Next Page" %}</a></h3>
{% endif %}
</div>
And the URL is /bgate/filter/

How to create a filter

We will start with designing a form which will contain fields we want to use. I've chosen "race", "class", "gender" and "level" fields. First tree are string, and the last one is integer. My form looks like this
<form method="post" action="." onsubmit="location.assign('/bgate/filter/' + document.szukaj.race.value + '/' + document.szukaj.klass.value + '/' + document.szukaj.gender.value + '/' + document.szukaj.level.value + '/'); return false;" name="szukaj">
<div class="content">
          <table>
           <tr class="rowA">
              <td class="first"><b>{% trans "Race" %}</b></td>
              <td><b>{% trans "Class" %}</b></td>
              <td><b>{% trans "Gender" %}</b></td>
              <td><b>{% trans "Levels" %}</b></td>
		<td> </td>
            </tr>
            <tr class="rowB">
              <td class="first"><select name="race">
              <option value="0">{% trans "All" %}</option>
               {% for i in races %}
               <option>{{ i }}</option>
               {% endfor %}
              </select></td>
              <td><select name="klass">
              <option value="0">{% trans "All" %}</option>
               {% for i in classes %}
                <option>{{ i }}</option>
               {% endfor %}
              </select></td>
              <td><select name="gender">
              <option value="0">{% trans "All" %}</option>
               {% for i in genders %}
                <option>{{ i }}</option>
               {% endfor %}
              </select></td>
              <td><select name="level">
              <option value="0">{% trans "All" %}</option>
              <option value="1">1-2</option>
              <option value="2">1-8</option>
              <option value="3">8-14</option>
              <option value="4">14-20</option>
              <option value="5">20+</option>
              </select></td>
              <td><input type="submit" name="search" class="button" value="{% trans "Search" %}" /></td>
            </tr>
</table></div></form>
Most important is the onsubmit form attribute:
onsubmit="location.assign('/bgate/filter/' + document.szukaj.race.value + '/' + document.szukaj.klass.value + '/' + document.szukaj.gender.value + '/' + document.szukaj.level.value + '/'); return false;"
The form isn't send ("return false;") but JavaScript will redirect us to a new URL using values from the form:
document.FORM_NAME.FIELD_NAME.value
We could use QueryString and GET but we would get not so nice URLs, and we wouldn't have data validation as with normal URLs, so this trick makes use of standard URLs to get all those goodies. The rest of the form are select fields. I could place all options in the form, but I didn't want it to get ugly. As for "level" I've choosen my own "values". Note: by "0" in all fields I mean "any valune" - no filter rule for this field. The view sends all the data:
def chr_filter(request, pagination_id=1):
	from django.views.generic.list_detail import object_list
	characters = Character.objects.all().order_by('-id')
	races = [_('Human'), _('Elf'), _('Half elf'), _('Dwarf'), _('Halfling'), _('Gnome'), _('Half Orc')]
	genders = [_('Male'), _('Female')]
	#let say that there is no / in those strings
	classes = [_('Mage'), _('Fighter'), _('Cleric'), _('Thief'), _('Bard'), _('Paladin'), _('Fighter/Mage'), _('Fighter/Cleric'), _('Fighter/Thief'), _('Fighter/Mage/Thief'), _('Druid'), _('Ranger'), _('Mage/Thief'), _('Cleric/Mage'), _('Cleric/Thief'), _('Fighter/Druid'), _('Fighter/Mage/Cleric'), _('Cleric/Ranger'), _('Sorcerer'), _('Monk')]
	return object_list(request, characters, paginate_by = 15, allow_empty = True, page = pagination_id, template_name = 'baldur/chr_filter.html', extra_context = {'races': races, 'genders': genders, 'classes': classes})
When I send the form I can get URL like /bgate/filter/Elf/Mage/Female/0/. It's time for URL mapping rules:
(r'^filter/$', 'views.chr_filter'),
(r'^filter/(?P<race>[\w\-_ ]+)/(?P<klass>[\w\-_]+)/(?P<gender>[\w\-_]+)/(?P<level>[0-9]+)/$', 'views.chr_filter'),
(r'^filter/(?P<race>[\w\-_ ]+)/(?P<klass>[\w\-_]+)/(?P<gender>[\w\-_]+)/(?P<level>[0-9]+)/(?P<pagination_id>(\d+))/$', 'views.chr_filter'),
One int and tree string fields. Now our form variables will be passed to a view which looks like this:
def chr_filter(request, pagination_id=1, race='0', klass='0', gender='0', level='0'):
	from django.views.generic.list_detail import object_list
	characters = Character.objects.all().order_by('-id')
	
	if race != '0':
		characters = characters.filter(race=race)
	if klass != '0':
		characters = characters.filter(main_class=klass)
	if gender != '0':
		characters = characters.filter(gender=gender)
	
	if level == '1':
		characters = characters.filter(levels__range=(1, 2))
	if level == '2':
		characters = characters.filter(levels__range=(1, 8))
	if level == '3':
		characters = characters.filter(levels__range=(8, 14))
	if level == '4':
		characters = characters.filter(levels__range=(14, 20))
	if level == '5':
		characters = characters.filter(levels__gte=20)
	
	races = [_('Human'), _('Elf'), _('Half elf'), _('Dwarf'), _('Halfling'), _('Gnome'), _('Half Orc')]
	genders = [_('Male'), _('Female')]
	classes = [_('Mage'), _('Fighter'), _('Cleric'), _('Thief'), _('Bard'), _('Paladin'), _('Fighter/Mage'), _('Fighter/Cleric'), _('Fighter/Thief'), _('Fighter/Mage/Thief'), _('Druid'), _('Ranger'), _('Mage/Thief'), _('Cleric/Mage'), _('Cleric/Thief'), _('Fighter/Druid'), _('Fighter/Mage/Cleric'), _('Cleric/Ranger'), _('Sorcerer'), _('Monk')]
	return object_list(request, characters, paginate_by = 15, allow_empty = True, page = pagination_id, template_name = 'baldur/chr_filter.html', extra_context = {'races': races, 'genders': genders, 'classes': classes})
I've added expected variables to the view with default value: , race='0', klass='0', gender='0', level='0' ("0" means no filter rule). Next I "select" all the characters (the SQL query isn't performed yet): characters = Character.objects.all().order_by('-id'), and then I can filter this QuerySet as I want using nifty ORM feature - I can add rules not in one command but in many (so it easy to add them in this case). Note that "level" filed doesn't return ints but strings. __range is between A and B, __gte - greater or equal than A.

Our view is working but it needs the last polish - form needs to remember it's values, and we have to make those pagination URLs. So we need to pass filter variables from the view to the template:
return object_list(request, characters, paginate_by = 15, allow_empty = True, page = pagination_id, template_name = 'baldur/chr_filter.html', extra_context = {'races': races, 'genders': genders, 'classes': classes, 'race': race, 'klass': klass, 'gender': gender, 'level': level})
Now we can use a trick like this:
<option{% ifequal i race %} selected="selected"{% endifequal %}>{{ i }}</option>
So our form looks like this:
<form method="post" action="." onsubmit="location.assign('/bgate/filter/' + document.szukaj.race.value + '/' + document.szukaj.klass.value + '/' + document.szukaj.gender.value + '/' + document.szukaj.level.value + '/'); return false;" name="szukaj">
<div class="content">
          <table>
           <tr class="rowA">
              <td class="first"><b>{% trans "Race" %}</b></td>
              <td><b>{% trans "Class" %}</b></td>
              <td><b>{% trans "Gender" %}</b></td>
              <td><b>{% trans "Levels" %}</b></td>
		<td> </td>
            </tr>
            <tr class="rowB">
              <td class="first"><select name="race">
              <option value="0">{% trans "All" %}</option>
               {% for i in races %}
               <option{% ifequal i race %} selected="selected"{% endifequal %}>{{ i }}</option>
               {% endfor %}
              </select></td>
              <td><select name="klass">
              <option value="0">{% trans "All" %}</option>
               {% for i in classes %}
                <option{% ifequal i klass %} selected="selected"{% endifequal %}>{{ i }}</option>
               {% endfor %}
              </select></td>
              <td><select name="gender">
              <option value="0">{% trans "All" %}</option>
               {% for i in genders %}
                <option{% ifequal i gender %} selected="selected"{% endifequal %}>{{ i }}</option>
               {% endfor %}
              </select></td>
              <td><select name="level">
              <option value="0">{% trans "All" %}</option>
              <option value="1"{% ifequal "1" level %} selected="selected"{% endifequal %}>1-2</option>
              <option value="2"{% ifequal "2" level %} selected="selected"{% endifequal %}>1-8</option>
              <option value="3"{% ifequal "3" level %} selected="selected"{% endifequal %}>8-14</option>
              <option value="4"{% ifequal "4" level %} selected="selected"{% endifequal %}>14-20</option>
              <option value="5"{% ifequal "5" level %} selected="selected"{% endifequal %}>20+</option>
              </select></td>
              <td><input type="submit" name="search" class="button" value="{% trans "Search" %}" /></td>
            </tr>
</table></div></form>
And pagination urls:
{% if has_previous %}
<h3><a href="/bgate/filter/{{ race }}/{{ klass }}/{{ gender }}/{{ level }}/{{ previous }}/">{% trans "Previous Page" %}</a></h3>
{% endif %}
{% if has_next %}
<h3><a href="/bgate/filter/{{ race }}/{{ klass }}/{{ gender }}/{{ level }}/{{ next }}/">{% trans "Next Page" %}</a></h3>
{% endif %}


Results

djfilter1
djfilter2
djfilter3
RkBlog

Programming in Python, 14 July 2008


Check out the new site at https://rkblog.dev.
Comment article
Comment article RkBlog main page Search RSS Contact