Widok filtrujący w Django
14 July 2008
Comments
Naszym celem będzie stworzenie widoku umożliwiającego filtrowanie wpisów z danego modelu-tabeli za pomocą formularza umożliwiającego określenie wielu parametrów :) Dodatkowo zastosujemy generyczny widok do stronicowania (tak by nie był to zwykły formularz i POST) (zobacz zrzuty ekranu na końcu artykułu)Jako przykład wykorzystam moją aplikację - katalog postaci Baldurs Gate. Model ma wiele pól, po których można filtrować:
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>
Tworzenie filtra
Tworzenie filtra zaczynamy od przygotowania formularza zawierającego pola, po których chcemy filtrować. Ja wybrałem pola "race" - Rasa, "class" - Klasa, "gender" - Płeć i "level" - Poziom postaci. Poza liczbowym poziomem pozostałe pola zawierają łańcuchy. Oto formularz:<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;"
Formularz nie jest wysyłany ("return false;") lecz JavaScript przenosi nas pod inny odnośnik wykorzystując wartości z formularza:
document.NAZWA_FORMULARZA.NAZWA_POLA.value
Dlaczego tak a nie użyć np. QueryString i zmiennych GET ? Dostajemy niezbyt ładne linki oraz nie mamy walidacji danych w GET, a powyższe rozwiązanie umożliwia dokładne określenie typów wartości jakie poszczególne zmienne mogą przyjmować. Reszta formularza to zwykłe pola Select. Poza poziomami listuję wszystkie możliwe rasy/klasy i płcie. Dla Poziomów zastosowałem własne "kryteria" podając przedziały poziomów i przypisując im określone wartości liczbowe. Mogłem dodać wszystkie opcje bezpośrednio do szablonu ale nie chciałem robić go nieczytelnym. Dodatkowo jak widać wartość "0" dla pola oznacza "Dowolny" - nie ma filtrowania dla danego pola. Widok przekazuje odpowiednie dane:
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})
Nasz widok i filtrowanie działa, ale trzeba wykończyć kod - dodać zapamiętywanie wartości przez formularz oraz dodać linki dla stronicowania. W tym celu do szablonu trzeba przekazać wartości zmiennych:
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 %}
Wynik
RkBlog
Comment article