メモ〜CreateViewやUpdateViewで外部キーを設定する方法


今回のお題

今回は、CreateViewUpdateViewなどの汎用ビューでインスタンスの外部キー由来のフィールドに値をセットする方法をまとめます。

やりたいことの例

以下のようなモデル設計で、Menuインスタンスの作成時にログイン中のユーザーが所属している店舗を外部キーとして設定したい。

class CustomUser(AbstractUser):
  shop = models.Foreignkey("shop.Shop", on_delete=models.PROTECT)
class Menu(models.Model):
  name = models.CharField(max_length=20)
  shop = models.Foreignkey("shop.Shop", on_delete=models.CASCASE)
class MenuForm(ModelForm):
  class Meta:
    model = Menu
    fields = ["name"]

フォームのフィールドとして指定してしまうと他の店舗を選択できてしまうので、その方法は取りたくない(バリデーションで対応できなくもないが、できれば自動で店舗情報が設定される形にしたい)。

結論

CreateViewUpdateViewに実装されているform_validメソッドをオーバーライドすることで対応可能。

class MenuCreateView(CreateView):
  template_name = "menu/create.html"
  model = Menu
  form_class = MenuForm
  def form_valid(self, form):
    form.instance.shop = self.request.user.shop

form_validメソッドはFormクラスやModelFormクラスのバリデーションがOKだった時に呼ばれてフォームを保存したのちにHttpResponseオブジェクトを返すメソッド。

なのでこのメソッドを上書きし、保存の直前に外部キーのデータがセットされるようにすれば良い。

フォームには外部キーのフィールド自体を用意していないので、店舗情報がなくてもバリデーションに引っかかることはない。

form_validメソッドについて

結論だけでは面白くないので、form_validメソッドの解説を一応。

CreateViewとUpdateViewはそれぞれ、

FormMixin > ModelFormMixin > BaseCreate(Update)View > Create(Update)View

というルートで継承を重ねており、その度にform_validメソッドに関する機能が追加されている。

具体的には、

  • FormMixinでform_validメソッドを定義(この段階ではHttpResponseオブジェクトを返す役割のみ)。
  • ModelFormMixinで、form_validメソッドが上書きされ、HttpResponseを返す前にフォーム内容が保存されるようになる。
  • ProcessFormView(BaseCreate/Updateviewのもう一つの継承元)のpostメソッド内で、form.is_valid()=trueの場合にのみform_validメソッドが呼び出されるように定義される。

という流れになっている。

class FormMixin(ContextMixin):
  def form_valid(self, form):
        """If the form is valid, redirect to the supplied URL."""
        return HttpResponseRedirect(self.get_success_url())
class ModelFormMixin(FormMixin, SingleObjectMixin):
  def form_valid(self, form):
        """If the form is valid, save the associated model."""
        self.object = form.save()
        return super().form_valid(form)
class ProcessFormView(View):
    """Render a form on GET and processes it on POST."""
    def get(self, request, *args, **kwargs):
        """Handle GET requests: instantiate a blank version of the form."""
        return self.render_to_response(self.get_context_data())

    def post(self, request, *args, **kwargs):
        """
        Handle POST requests: instantiate a form instance with the passed
        POST variables and then check if it's valid.
        """
        form = self.get_form()
        if form.is_valid():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)

    # PUT is a valid HTTP verb for creating (with a known URL) or editing an
    # object, note that browsers only support POST for now.
    def put(self, *args, **kwargs):
        return self.post(*args, **kwargs)