Filter View in Django
Check out the new site at https://rkblog.dev.
14 July 2008
Comments
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
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')
<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>
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>
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})
(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'),
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})
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})
<option{% ifequal i race %} selected="selected"{% endifequal %}>{{ i }}</option>
<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>
{% 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



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