今回の話題
今回は、CreateViewの中で多数対多数の関連付けを差し込もうとして困ったという話をします。
やろうとしたこと
- お菓子屋さんのサイトを作成しており、モデルとして
User
,Menu
,Shop
,Campaign
,Order
が存在。 -
Orderモデル
は他の4つを外部キーとしてもち、ユーザーがある商品を注文した時点で購入店舗・購入商品・購入者・実施中のキャンペーンが全てOrderモデルインスタンス
の情報として記録される。 - 商品名・購入店舗・キャンペーンなどに関してはフォームで入力するのではなく、商品ページから購入ページに飛ぶことで自動で設定されるようにする。
モデルなど
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
です。
from django.forms import ModelForm
from .models import Order
class OrderForm(ModelForm):
class Meta:
model = Order
fields = ["payment"]
上記の通り外部キー設定している情報に関してはフォームでは入力せずにパラメータで拾います。
キャンペーンや店舗などはメニューと紐づいているので、urlから拾うのはmenu_id
のみで大丈夫です。
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
メソッドを上書きしました。
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
を使うように変更しました。
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
メソッドの中でリレーションを設定する以外に良い方法が思いつきませんでした。
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()
の後に実行されるのでその点では安心ですが、今回のように他のアプリケーションフォルダからインポートするとコードが長くなって見栄えが悪いですね・・・