Djangoroidの奮闘記

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

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

これで、新規で登録ができようになる。

まとめ

ちょっと今回はヘビーだった。。。

  1. formを作る
  2. formsetを作る(formset_factory()関数)
  3. viewにformをimportする
  4. viewから、templateにformをcontextとして渡す

というのが大まかな流れになるか。

注意したい点は、save()のタイミングとかかな。