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 %}