Django e-commerce part19 Formset for Inventry
在庫表示のための、Formset
フォームセットについての説明は、以下のサイト
フォームセット (formsets) — Django 1.4 documentation
フォームセット=フォームの集まり
views.pyにvariationlistviewを追加する
- ProductListViewをそのままコピペして、class名と、model, queryset, superの箇所を変更する。
- 不要な箇所を削除する。
- あと、testで、get_querysetファンクションにprint(self.kwargs)をいれておく? なぜかはよくわからん。
class VariationListView(ListView): model = Variation queryset = Variation.objects.all() def get_context_data(self, *args, **kwargs): context = super(VariationListView, self).get_context_data(*args, **kwargs) context["now"] = timezone.now context["query"] = self.request.GET.get("q") return context def get_queryset(self, *args, **kwargs): qs = super(VariationListView, self).get_queryset(*args, **kwargs) query = self.request.GET.get("q") print (self.kwargs) return qs
urls.py を編集する。
VariationListViewをimportするのを忘れない。 あと、inventoryは基本、product_detail のコピペを編集でOK
... from .views import ProductDetailView, ProductListView, VariationListView ... url(r'^(?P<pk>\d+)/inventory/$', VariationListView.as_view(), name='product_inventory'), ...
products/variation_list.html を作成する
ひとまず、product_list.htmlをコピペする。 現状では、/products/1/inventory/ も、/products/2/inventory/ も全く同じリストが表示されてしまうため、ここを変更したい。productkeyで絞り込みできるようにすると思われる。
ここで、さっき、viewに設定しておいた、print(self.kwargs)が役に立つ。 コマンドラインを見ると、{'pk': '1'} とかいう感じで表示されていると思う。 これを使って絞り込みをしていく。
class VariationListView(ListView): model = Variation queryset = Variation.objects.all() def get_queryset(self, *args, **kwargs): qs = super(VariationListView, self).get_queryset(*args, **kwargs) query = self.request.GET.get("q") product_pk = self.kwargs.get("pk") if product_pk: product = get_object_or_404(Product, pk=product_pk) queryset = Variation.objects.filter(product=product) return queryset
解説: * product_pk = self.kwargs.get("pk") で、product/1/inventory にアクセスした時に、ゲットできる、productのpkを代入する. * product に、Productとmodelのpkで絞り込んだobjectを代入する。(ない場合は、raise Httperror404) * queryset に、productで絞り込んだvariationのobjectsを代入する。 * querysetを返す。
VariationListVIewを少し整理する
class VariationListView(ListView): model = Variation queryset = Variation.objects.all() def get_context_data(self, *args, **kwargs): context = super(VariationListView, self).get_context_data(*args, **kwargs) return context def get_queryset(self, *args, **kwargs): product_pk = self.kwargs.get("pk") if product_pk: product = get_object_or_404(Product, pk=product_pk) queryset = Variation.objects.filter(product=product) return queryset def post(self, request, *args, **kwargs): raise Http404
post メソッドは、class based viewに設定されているメソッドの1つ。多分上書きするための準備だと思われる。詳しくは以下のサイトを参照:
Introduction to class-based views | Django documentation | Django
products/forms.py を作成する
from django import forms from .models import Variation class VariationInventoryForm(forms.ModelForm): class Meta: model = Variation fields = [ "price", "sale_price", "inventory" ]
forms.pyに、modelformset_factoryを導入する。
どうも、modelform_factory() という便利なfunctionがあるらしい。modelformset_factory の方を今回は使う。
Creating forms from models | Django documentation | Django
from django import forms from django.forms import modelformset_factory from .models import Variation class VariationInventoryForm(forms.ModelForm): class Meta: model = Variation fields = [ "price", "sale_price", "inventory" ] VariationInventoryFormset = modelformset_factory(Variation, form=VariationInventoryForm, extra=2)
products/views.py を編集する。
... from .forms import VariationInventoryFormset ... def get_context_data(self, *args, **kwargs): context = super(VariationListView, self).get_context_data(*args, **kwargs) context["formset"] = VariationInventoryFormset() return context ... def post(self, request, *args, **kwargs): print request.POST raise Http404 ...
- context["formset"] = VariationInventoryFormset()を追加する
- print (request.POST) を表示する。
variation_list.html に追加する。
... <form method="POST" action=""> {% csrf_token %} {{ formset.as_p }} <input type="submit" value='Update' class='bnt' /> </form> ...
これで表示すると、表示はされるが、すべてのvariationsが表示されてしまう。 そのため、これをproductで絞り込みをしたい。
views.py を修正
context["formset"] = VariationInventoryFormset(queryset=self.get_queryset())
get_queryset()は、さっき編集した通り、product_pkで絞込み機能が付いている。 そのため、querysetは、product_pkで絞り込まれたquerysetが渡される?
これで、inventory_listを表示してみると、無事表示される。
ちなみに、update ボタンを押すと、postにprint (request.POST) を設定しておいたので、以下のような表示がされると思われる。
<QueryDict: {'form-1-inventory': ['2'], 'form-0-inventory': ['6'], 'form-0-price': ['100'], 'csrfmiddlewaretoken': ['*************'], 'form-1-sale_price': ['35'], 'form-TOTAL_FORMS': ['2'], 'form-1-id': ['4'], 'form-MIN_NUM_FORMS': ['0'], 'form-MAX_NUM_FORMS': ['1000'], 'form-0-id': ['3'], 'form-INITIAL_FORMS': ['2'], 'form-1-price': ['70'], 'form-0-sale_price': ['50']}>
上記を見ると、form-0 とか、form-1とかが出てると思う。formsetのため、複数のフォームを持つことができていることがわかる。
variation_list.html にフォームを複数表示する。
<form method="POST" action=""> {% csrf_token %} {{ formset.management_form }} {% for form in formset %} {{ form.instance.product.title }} {{ form.instance.title }} {{ form.as_p }} {% endfor %} <input type="submit" value='Update' class='bnt' /> </form>
- formset.management_form については、難しくてよく分からない。詳細は以下のリンク参照だが、用途としては、javascriptとかで動的に、formの数を増やす時とかに必要なものらしい。
フォームセット (formsets) — Django 1.4 documentation
views.py VariationListViewを修正する
from django.contrib import messages ... formset = VariationInventoryFormset(request.POST, request.FILES) print (request.POST) if formset.is_valid(): formset.save(commit=False) for form in formset: form.save() messages.success(request, "在庫の状況が更新されました") return redirect("products") raise Http404 ...
- formsetが、有効な場合は、処理を行い、それ以外の場合は、Http404を返す。
- save(commit=False)は、データベースに保存する前の状態。公式サイトの引用は以下のとおり
save() メソッドはオプション commit キーワード引数を持っています。こ の引数には True または False を指定します。 save() を commit=False で呼び出すと、データベースに保存する前のモデルオブジェクト を返します。返されたオブジェクトに対して、最終的に save() を呼び出すか どうかは自由です。この機能は、オブジェクトを実際に保存する前に何らかの処理 を行いたい場合や、特殊なオプション付きで モデルオブジェクトを保存したい 場合に便利 です。 commit はデフォルトでは True に設定されています。
- formset は、formの集合体(リスト?)と考えて、forで1つ1つsaveしていく。
forms.py などを編集していく
class VariationInventoryForm(forms.ModelForm): class Meta: model = Variation fields = [ "title", "price", "sale_price", "inventory", "active" ] VariationInventoryFormset = modelformset_factory(Variation, form=VariationInventoryForm, extra=1)
extra=1 にして、新規登録ができるようにしてる。 ただ、このまま新規登録しても、IntegrityError at /products/3/inventory/ が出てしまう。 NOT NULL constraint failed: products_variation.product_id というerror内容
多分これは、productsフィールドが表示されていないため、productsには何も入っていない状態で新規登録をしようとしているため。
これを修正するには、views.py のsave()の所あたりを修正する。
views.py のVariationListViewを修正する。
def post(self, request, *args, **kwargs): formset = VariationInventoryFormset(request.POST, request.FILES) print (request.POST) if formset.is_valid(): formset.save(commit=False) for form in formset: new_item = form.save(commit=False) # formのinstanceをnew_itemに代入する。 product_pk = self.kwargs.get("pk") # urlのpkを、product_pkに代入 product = get_object_or_404(Product, pk=product_pk) # Productより、product_pkに一致するobjectをゲットして、productに代入する。 new_item.product = product # new_item(form instance)のproductに、productを代入する。 new_item.save() messages.success(request, "在庫の状況が更新されました") return redirect("products") raise Http404
これで、新規で登録ができようになる。
まとめ
ちょっと今回はヘビーだった。。。
- formを作る
- formsetを作る(formset_factory()関数)
- viewにformをimportする
- viewから、templateにformをcontextとして渡す
というのが大まかな流れになるか。
注意したい点は、save()のタイミングとかかな。