0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

メモ〜テンプレートビューの中で多数対多数の関連付けを差し込もうとして困った話

Posted at

今回の話題

今回は、CreateViewの中で多数対多数の関連付けを差し込もうとして困ったという話をします。

やろうとしたこと

  • お菓子屋さんのサイトを作成しており、モデルとしてUser, Menu, Shop, Campaign, Orderが存在。
  • Orderモデルは他の4つを外部キーとしてもち、ユーザーがある商品を注文した時点で購入店舗・購入商品・購入者・実施中のキャンペーンが全てOrderモデルインスタンスの情報として記録される。
  • 商品名・購入店舗・キャンペーンなどに関してはフォームで入力するのではなく、商品ページから購入ページに飛ぶことで自動で設定されるようにする。

モデルなど

order/models.py
from django.db import models
from django.contrib.auth import get_user_model

# Create your models here.
class Order(models.Model):
  user = models.ForeignKey(
    get_user_model(),
    verbose_name="購入者名",
    on_delete=models.CASCADE
  )
  price = models.IntegerField(
    verbose_name="価格"
  )
  shop = models.ForeignKey(
    "shop.Shop",
    verbose_name="購入店舗名",
    on_delete=models.PROTECT
  )
  menu = models.ForeignKey(
    "menu.Menu",
    verbose_name="商品名",
    on_delete=models.PROTECT
  )
  created_at = models.DateTimeField(
    verbose_name="注文日時",
    auto_now_add=True,
  )
  payment = models.IntegerField(
    verbose_name="お支払い方法",
    choices=[
      (0, "先払い(カードのみ)"),
      (1, "当日支払い")
    ]
  )
  campaigns = models.ManyToManyField(
    "campaign.Campaign"
  )

見ての通り、CampaignのみManyToManyで他はForeignKeyです。

order/forms.py
from django.forms import ModelForm
from .models import Order
class OrderForm(ModelForm):
  class Meta:
    model = Order
    fields = ["payment"]

上記の通り外部キー設定している情報に関してはフォームでは入力せずにパラメータで拾います。

キャンペーンや店舗などはメニューと紐づいているので、urlから拾うのはmenu_idのみで大丈夫です。

order/urls.py
from django.urls import path
from . import views
urlpatterns = [
    path("create/<int:menu_id>/", views.OrderCreateView.as_view(), name="create"),
    path("detail/<int:pk>", views.OrderDetailView.as_view(), name="detail"),
]

ビューでの処理

とりあえずはフォームに外部キーを差し込む場合の基本に則り、form_validメソッドを上書きしました。

order/views.py
class OrderCreateView(OnlyCustomerMixin, CreateView):
  template_name = "order/create.html"
  model = Order
  form_class = OrderForm

  def get_context_data(self, **kwargs):
    context = super().get_context_data(**kwargs)
    menu_id = self.kwargs["menu_id"]
    import sys
    sys.path.append("..")
    from menu.models import Menu
    menu = Menu.objects.get(pk=menu_id)
    context["menu"] = menu
    now_campaigns = menu.get_now_campaigns()
    discount_price = menu.get_discount_price()
    context["now_campaigns"] = now_campaigns
    context["discount_price"] = discount_price
    return context
  
  def form_valid(self, form):
    menu_id = self.kwargs["menu_id"]
    import sys
    sys.path.append("..")
    from menu.models import Menu
    menu = Menu.objects.get(pk=menu_id)
    form.instance.menu = menu
    form.instance.shop = menu.shop
    form.instance.user = self.request.user
    form.instance.price = menu.get_discount_price()
    form.instance.campaigns = menu.get_now_campaigns()
    return super().form_valid(form)

  def get_success_url(self) -> str:
    return reverse_lazy("order:detail", kwargs={"pk": self.object.pk })

menu.get_now_campaigns()menu.get_discount_price()はそれぞれ適用中のキャンペーンとキャンペーン込みの価格を返すメソッドです。

で、これで動かしてみた結果がこちら。

TypeError: Direct assignment to the forward side of a many-to-many set is prohibited. Use campaigns.set() instead.

つまりManyToManyのリレーションを組んでいる時には、代入ではなくsetを使えということらしい。

で、setを使うように変更しました。

views.py
  def form_valid(self, form):
    menu_id = self.kwargs["menu_id"]
    import sys
    sys.path.append("..")
    from menu.models import Menu
    menu = Menu.objects.get(pk=menu_id)
    form.instance.menu = menu
    form.instance.shop = menu.shop
    form.instance.user = self.request.user
    form.instance.price = menu.get_discount_price()
    form.instance.campaigns.set(menu.get_now_campaigns()) # ここを変更
    return super().form_valid(form)

その結果がこちら。

ValueError: "<Order: Order object (None)>" needs to have a value for field "id" before this many-to-many relationship can be used.

idがないとManyToManyの関連付けができないよ。

とのこと。

form_valid()メソッドの途中だとまだform.save()が実行されていないのでorderインスタンスがまだidを持っていない、すなわちform.save()よりも後に実行されるメソッドの中でリレーションを設定するしかないということですね。

色々と考えましたが、get_success_urlメソッドの中でリレーションを設定する以外に良い方法が思いつきませんでした。

order/views.py
  def get_success_url(self) -> str:
    menu_id = self.kwargs["menu_id"]
    import sys
    sys.path.append("..")
    from menu.models import Menu
    menu = Menu.objects.get(pk=menu_id)
    self.object.campaigns.set(menu.get_now_campaigns())
    return reverse_lazy("order:detail", kwargs={"pk": self.object.pk })

ここなら確実にform.save()の後に実行されるのでその点では安心ですが、今回のように他のアプリケーションフォルダからインポートするとコードが長くなって見栄えが悪いですね・・・

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?