Djangoroidの奮闘記

python,django,angularJS1~三十路過ぎたプログラマーの奮闘記

Django e-commerce part76 django-filter

django-filterを使ってみる

公式ドキュメント:

django-filter — django-filter 0.15.2 documentation

まずはpip install

pip install django-filter

settings/local.pyに設定をかきこむ

INSTALLED_APPS = (
...
    #third party apps
    'django_filters',
...
  • django_filetersと、アンダースコアになっている点に注意

makemigrations. migrateする

products/views.pyに追記してみる。

from django_filters import FilterSet #django_filtersからimport
...
class ProductFilter(FilterSet):
    class Meta:
        model = Product
        fields = [
            'title',
            'description',
        ]
...

これで、ProductFilter classを作成できる。

ProductFilterを使ったfunction based viewを作ってみる。

def product_list(request):
    qs = Product.objects.all()
    f = ProductFilter(request.GET, queryset=qs)
    return render(request, "products/product_list.html", {"object_list": f})

ポイント

  • qs(queryset)に、Product のobjectsのリストを代入
  • fに、qsのquerysetに、request.GETでフィルターをかけたobjectsのリストを代入
  • request, template, contextをrenderして返す。contextには、ProductFilterで、フィルターをかけたquerysetのfをobject_listとして渡す。(すでにtemplateのproduct_listに、object_listがセットしてあるため。なので、ここは名前は何でもいい)

urls.py に追記

urlpatterns = [
    url(r'^$', 'products.views.product_list', name='products'),

function based viewのため、class based viewとはちょっと違うため注意。

この時点で、/products/?title=名前 とかで検索可能。

ProductFilterをさらに修正する。

from django_filters import FilterSet, CharFilter, NumberFilter

class ProductFilter(FilterSet):
    category = CharFilter(name='categories__title', lookup_type='icontains')
    class Meta:
        model = Product
        fields = [
            'category',
            'title',
            'description',
        ]

これでcategoryもfilterできる

orderの順番も変更してみる。

def product_list(request):
    qs = Product.objects.all()
    ordering = request.GET.get("ordering")
    if ordering:
        qs = Product.objects.all().order_by(ordering)
        return qs
    f = ProductFilter(request.GET, queryset=qs)
    return render(request, "products/product_list.html", {"object_list": f})

filtermixinを作成してみる。

from django.core.exceptions import ImproperlyConfigured

class FilterMixin(object):
    filter_class = None
    search_ordering_param = "ordering"

    def get_queryset(self, *args, **kwargs):
        try:
            qs = super().get_queryset(*args, **kwargs)
        except:
            raise ImproperlyConfigured("フィルターをかけるクエリセットがありません。")

ポイント

  • ListViewに継承してもらうのを想定しているので、class methodの、get_querysetを上書きする。
  • filter_class = None は、filter_class継承しないという意味??
  • search_ordering_paramに、orderingを代入する。
  • querysetがない場合のerrorも表示させるようにしておく。

さらに、filtermixinに、functionを追加

    def get_context_data(self, *args, **kwargs):
        context = super().get_context_data(*args, **kwargs)
        qs = self.get_queryset()
        ordering = self.request.GET.get(self.search_ordering_param)
        if ordering:
            qs = qs.order_by(ordering)
        filter_class = self.filter_class
        if filter_class:
            f = filter_class(self.request.GET, queryset=qs)
            context["object_list"] = f
        return context

ProductListViewに、filtermixinを継承、filter_classにProductFilterを代入

class ProductListView(FilterMixin, ListView):
    model = Product
    queryset = Product.objects.all()
    filter_class = ProductFilter 
...

これで、productlistviewでも同じ動作をするようになる。

titleもicontainsにしてみる

class ProductFilter(FilterSet):
    title = CharFilter(name='title', lookup_type='icontains')
    category = CharFilter(name='categories__title', lookup_type='icontains')
    class Meta:
        model = Product
        fields = [
            'category',
            'title',
            'description',
        ]

max_price,min_priceもつけてみる。

class ProductFilter(FilterSet):
    title = CharFilter(name='title', lookup_type='icontains')
    category = CharFilter(name='categories__title', lookup_type='icontains')
    min_price = NumberFilter(name='price', lookup_type='gte')#greater than equal
    max_price = NumberFilter(name='price', lookup_type='lte')#less than equal
    class Meta:
        model = Product
        fields = [
            'category',
            'title',
            'description',
            'min_price',
            'max_price',
        ]

variationもつけてみる。

    min_price = NumberFilter(name='variation_price', lookup_type='gte')#greater than equal

上記だとerrorになる。 errorの内容は以下のような感じです。

Cannot resolve keyword 'variation_price' into field. Choices are: active, categories, default, default_id, description, id, price, productfeatured, productimage, title, variation

そのため、アンダースコアを2個でセットする。

    min_price = NumberFilter(name='variation__price', lookup_type='gte')#greater than equal
    max_price = NumberFilter(name='variation__price', lookup_type='lte')#less than equal

distinct= True (重複を削除する)をつける

class ProductFilter(FilterSet):
    title = CharFilter(name='title', lookup_type='icontains', distinct=True)
    category = CharFilter(name='categories__title', lookup_type='icontains', distinct=True)
    min_price = NumberFilter(name='variation__price', lookup_type='gte', distinct=True)#greater than equal
    max_price = NumberFilter(name='variation__price', lookup_type='lte', distinct=True)#less than equal
    class Meta:
        model = Product
        fields = [
            'category',
            'title',
            'description',
            'min_price',
            'max_price',
        ]

products/forms.py に、ProductFilterFormを作成する。

class ProductFilterForm(forms.Form):
    category = forms.CharField(required=False)

products/views.py のProductListViewに追記

class ProductListView(FilterMixin, ListView):
...
    def get_context_data(self, *args, **kwargs):
...
        context["filter_form"] = ProductFilterForm()

contextのfilter_formを、product_list.htmlに表示させる。

{% extends "base.html" %}
{% load crispy_forms_tags %}

{% block content %}

<div class='col-sm-2'>
<form method="GET" action="{% url 'products' %}">
{{ filter_form|crispy }}
<input type='submit' value='フィルター' class='btn btn-default'>
</form>
<a href="{% url 'products' %}">フィルターをクリアする</a>
</div>

<div class='col-sm-8'>
<h1>全ての商品 <small><a href='{% url 'categories' %}'>カテゴリー</a></small> </h1>
{% include "products/products.html" with object_list=object_list %}
</div>

{% endblock %}

さらにこのfilterformに、qの検索も追加する。

<div class='col-sm-2'>
<form method="GET" action="{% url 'products' %}">
{{ filter_form|crispy }}
<input type='hidden' name='q' value='{{ request.GET.q }}'/>
<input type='submit' value='フィルター' class='btn btn-default'>
</form>

これは例えば、以下のようなことが可能になる。

http://127.0.0.1:8000/products/?category=electronics&q=Designer

navbar.html

ただこのままでは、category=electronicsが残ってしまうため、navbarのサーチボックスにはqの値がセットされるようにしておく。

            <div class="form-group">
                <input type="text" class="form-control" placeholder="Search" name="q" value='{{ request.GET.q }}'>
            </div>

forms.py を修正する

CAT_CHOICES = (
    ('electronics', 'Electronics'),
    ('accessories', 'Accessories'),
)

class ProductFilterForm(forms.Form):
    q = forms.CharField(label='Search', required=False)
    category_id = forms.ModelMultipleChoiceField(
        label='Category',
        queryset=Category.objects.all(),
        widget=forms.CheckboxSelectMultiple,
        required=False)
    # category_title = forms.ChoiceField(
    #     label='Category',
    #     choices=CAT_CHOICES,
    #     widget=forms.CheckboxSelectMultiple,
    #     required=False)
    max_price = forms.DecimalField(decimal_places=0, max_digits=12, required=False)
    min_price = forms.DecimalField(decimal_places=0, max_digits=12, required=False)

views.py を修正

class ProductListView(FilterMixin, ListView):
...
        context["filter_form"] = ProductFilterForm(data=self.request.GET or None)
        return context
  • ProductFilterForm(data=self.request.GET or None) で、dataに、request.GETをセットした状態にできる。

product_list.html

{% extends "base.html" %}
{% load crispy_forms_tags %}

{% block content %}

<div class='col-sm-2'>
<form method="GET" action="{% url 'products' %}">
{{ filter_form|crispy }}
<input type='hidden' name='q' value='{{ request.GET.q }}'/>
<input type='submit' value='フィルター' class='btn btn-default'>
</form>
<a href="{% url 'products' %}">フィルターをクリアする</a>
</div>

<div class='col-sm-8'>
<h1>全ての商品 <small><a href='{% url 'categories' %}'>カテゴリー</a></small> </h1>

{% if object_list.count == 0 %}

該当する商品は見つかりませんでした。

{% else %}

{% include "products/products.html" with object_list=object_list %}

{% endif %}
</div>

{% endblock %}