zhegujiacheng
@zhegujiacheng

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

【Django】inlineformsetを用いたFormsetの更新・削除機能の実装について

解決したいこと

inlineformset_factoryを使用したFormsetの更新・削除機能を実装したい

Djangoで飲食店のメニュー登録アプリを作成しています。
メニューの追加をする際にメニューに対してinlineformsetを使ってオプションを付ける機能を実装したのですが、
更新・削除機能がどうしてもうまく行かず苦戦しています。

実装した機能は「ラーメン」というメニューに対して、「大盛り」などのオプションを選択できるようにするような機能です。

解決方法がわかる方がいましたら教えて下さい。

発生している問題・エラー

メニューを作成する際に一緒にオプションを作成する時はうまくデータベースに反映されるのですが、
一度データベースに反映されたものを更新・削除する際に項目が倍になってしまいます。

例えば、ラーメンというメニューに対して大盛りのオプションを付けた後、特盛りに修正しようとすると、
大盛りと特盛りが2つ並んでしまいます。

問題・エラーが起きている画像
【メニュー追加画面】
スクリーンショット 2021-08-25 23.05.23.png
【ラーメンを追加した後の修正画面】

スクリーンショット 2021-08-25 23.07.10.png
ここで「大盛り:100円」を「特盛り:200」に修正すると、、、
スクリーンショット 2021-08-25 23.13.09.png
大盛りと特盛りに、、、というか更新を押したら大盛りが2つになります。(削除をチェックして更新をしても大盛りが2つに)
スクリーンショット 2021-08-25 23.16.09.png
解決方法がわかる方ぜひ教えていただきたいです。

該当するソースコード

models.py

# メニューカテゴリ
class MenuCategories(BaseModel):
  name = models.CharField(max_length=50, verbose_name='メニューカテゴリ')
  store = models.ForeignKey(
    Stores, on_delete=models.CASCADE, null=True, blank=True, verbose_name='ストアID'
  )

  class Meta:
    db_table = 'menu_categories'

  def __str__(self):
    return self.name


# メニュー
class Menus(BaseModel):
  name = models.CharField(max_length=50, verbose_name='メニュー名')
  price = models.PositiveIntegerField(verbose_name='価格')
  description = models.CharField(max_length=255, null=True, verbose_name='説明')
  is_option = models.BooleanField(default=False, verbose_name='オプションの有無')
  category = models.ForeignKey(MenuCategories, on_delete=models.CASCADE, null=True, verbose_name='メニュカテゴリID') # メニューカテゴリに紐付け
  store = models.ForeignKey(Stores, on_delete=models.CASCADE, verbose_name='ストアID') # お店に紐付け
  picture = models.FileField(default='no_image.png', upload_to='menu_pictures/', verbose_name='写真')

  class Meta:
    db_table = 'menus'

  def __str__(self):
    return self.name

class MenuOptions(BaseModel):
  name = models.CharField(max_length=255, null=True, verbose_name='オプション名')
  price = models.PositiveIntegerField(null=True, verbose_name='価格')
  menu = models.ForeignKey(Menus, on_delete=models.CASCADE, null=True, verbose_name='メニューID')
  class Meta:
    db_table = 'menu_options'
# オプション追加フォーム
class AddMenuOptionForm(forms.ModelForm):
  name = forms.CharField(label='オプション', widget=forms.TextInput(attrs={'placeholder':'オプション名を追加してください'}))

  class Meta:
    model = MenuOptions
    fields = ['name', 'price']

  def __init__(self, *args,**kwargs):
    super(AddMenuOptionForm, self).__init__(*args, **kwargs)
    for field in self.fields.values():
      field.widget.attrs['class'] = 'form-control'

AddMenuOptionFormSet = forms.inlineformset_factory(
  parent_model=Menus,
  model=MenuOptions,
  form=AddMenuOptionForm,
  # fields=['name','price'],
  extra=1,
  can_delete=False, #Falseで削除ボタンなし
)

UpdateMenuOptionFormSet = forms.inlineformset_factory(
  parent_model=Menus,
  model=MenuOptions,
  form=AddMenuOptionForm,
  fields=['name','price'],
  # extra=1,
  can_delete=True,
)

# メニュー追加フォーム
class CreateMenuForm(forms.ModelForm):
  name = forms.CharField(label='メニュー名', widget=forms.TextInput(attrs={'placeholder':'メニュー名を入力してください'}))
  price = forms.IntegerField(label='価格(税込み)', widget=forms.TextInput(attrs={'placeholder':'半角数字で入力してください'}))
  category = forms.ModelChoiceField(label='メニューカテゴリ', queryset=MenuCategories.objects.all())
  description = forms.CharField(label='説明', widget=Textarea(attrs={'placeholder':'メニューの説明を入力してください'}) ) 

  formset_class = AddMenuOptionFormSet

  class Meta:
    model = Menus
    fields = ['name', 'price', 'category', 'description', 'picture']

  # categoryフォームの初期値をユーザーのストアに紐付けたい
  def __init__(self, *args,**kwargs):
    store_id = kwargs.pop('store_id')
    super(CreateMenuForm,self).__init__(*args,**kwargs)
    if store_id:
      self.fields['category'].queryset = MenuCategories.objects.filter(store_id=store_id)
    for field in self.fields.values():
      field.widget.attrs['class'] = 'form-control'

  def save(self):
    menu = super().save(commit=False)
    menu.store_id = self.store_id
    menu.create_at = datetime.now()
    menu.save()
    return menu

"""# メニュー更新フォーム"""
class UpdateMenuForm(forms.ModelForm):
  name = forms.CharField(label='メニュー名', widget=forms.TextInput(attrs={'placeholder':'メニュー名を入力してください'}))
  price = forms.IntegerField(label='価格(税込み)', widget=forms.TextInput(attrs={'placeholder':'半角数字で入力してください'}))
  category = forms.ModelChoiceField(label='メニューカテゴリ', queryset=MenuCategories.objects.all())
  description = forms.CharField(label='説明', widget=Textarea(attrs={'placeholder':'メニューの説明を入力してください'}) ) 

  formset_class = UpdateMenuOptionFormSet

  class Meta:
    model = Menus
    fields = ['name', 'price', 'category', 'description', 'picture']

  # categoryフォームの初期値をユーザーのストアに紐付け
  def __init__(self, *args,**kwargs):
    store_id = kwargs.pop('store_id')
    super(UpdateMenuForm,self).__init__(*args,**kwargs)
    if store_id:
      self.fields['category'].queryset = MenuCategories.objects.filter(store_id=store_id)
    for field in self.fields.values():
      field.widget.attrs['class'] = 'form-control'

  def save(self):
    menu = super().save(commit=False)
    menu.store_id = self.store_id
    menu.create_at = datetime.now()
    menu.save()
    return menu
"""# メニュー追加画面  """
class CreateMenuView(LoginRequiredMixin, CreateView):
  model = Menus
  template_name = 'stores/mystore/dashboards/create_menu.html'
  form_class = CreateMenuForm
  success_url = reverse_lazy('stores:menu_list')

  def get_context_data(self, **kwargs):
    context = super().get_context_data(**kwargs)
    formset = AddMenuOptionFormSet()
    context['formset'] = formset
    return context

  # 現在のユーザーをFormに渡す
  def get_form_kwargs(self):
    kwargs = super(CreateMenuView, self).get_form_kwargs()
    kwargs['store_id'] = self.request.user.stores.id
    return kwargs


  def form_valid(self, form):
          # store_idに現在のuser.store_idを登録  
    form.store_id = self.request.user.stores.id
    menu = form.save()
    formsets = AddMenuOptionFormSet(data=self.request.POST, instance=menu) #formsetのインスタンスを作成
    if formsets.is_valid():
      for formset in formsets:
        formset.save()
        menu.is_option = True
    messages.success(self.request, 'メニューを追加しました')
    return super().form_valid(form)


"""# メニュー修正画面"""
class UpdateMenuView(LoginRequiredMixin, UpdateView):
  model = Menus
  template_name = 'stores/mystore/dashboards/update_menu.html'
  form_class = UpdateMenuForm
  success_url = reverse_lazy('stores:menu_list')

  def get_form_kwargs(self):
    kwargs = super(UpdateMenuView, self).get_form_kwargs()
    kwargs['store_id'] = self.request.user.stores.id
    return kwargs

  # メニューを取得する
  def get_context_data(self, **kwargs):
    context = super().get_context_data(**kwargs)
    menu_id = self.kwargs.get('pk')
    menu = Menus.objects.get(id=menu_id)
    initial = []
    # is_optionがTrueのとき
    if menu.is_option:
      # menuに紐づくoptionを取得
      options = MenuOptions.objects.filter(menu_id=menu_id)
      # optionをリストで格納
      for option in options:
        initial.append({'name': option.name, 'price': option.price})
    # formsetの初期値を設定
    if initial:
      UpdateMenuOptionFormSet = forms.inlineformset_factory(
        parent_model=Menus,
        model=MenuOptions,
        form=AddMenuOptionForm,
        extra=len(initial),
        can_delete=True,
      )
      formset = UpdateMenuOptionFormSet(initial=initial)
    else:
      UpdateMenuOptionFormSet = forms.inlineformset_factory(
        parent_model=Menus,
        model=MenuOptions,
        form=AddMenuOptionForm,
        extra=1,
        can_delete=True,
      )
      formset = UpdateMenuOptionFormSet()
    context['formset'] = formset # オプションForm
    context['menu'] = menu
    return context

#↓おそらくif formset.is_valid():以降が原因だと思うのですが、正しい記述がかわかりません。 

  def form_valid(self, form):
    # store_idに現在のuser.store_idを登録  
    form.store_id = self.request.user.stores.id
    menu = form.save()
    formset = UpdateMenuOptionFormSet(data=self.request.POST or None, instance=menu) #formsetのインスタンスを作成
    if formset.is_valid():
      formset.save(commit=False)
      # 削除チェックがついたfileを取り出して削除
      for file in formset.deleted_objects:
        file.delete()

      # 新たに作成されたfileと更新されたfileを取り出し、ユーザーを紐づけて保存
      for file in formset:
        file.menu_id = menu.id
        file.save()
      menu.is_option = True # formsetが追加されたときはis_optionをTrueにする
    else:
      menu.is_option = False # formsetが空の時はis_optionをFalseにする
<form action="" method="POST" enctype="multipart/form-data">
  {% csrf_token %}
  {{ form.category|as_crispy_field }}
  <a class="btn btn-secondary" href="{% url 'stores:add_menu_category' %}">カテゴリ追加</a>
  {{ form.name|as_crispy_field }}
  {{ form.price|as_crispy_field }}
  {{ form.description|as_crispy_field }}

  {{ form.picture|as_crispy_field }}
  <ul>
    {{ formset.management_form }}
      {% for form in formset %}
          <li id="option_form">
            {{ form.as_p }}
          </li>
    {% endfor %}
  </ul>
  <div>
    <input class="mt-2 btn btn-primary" type="submit" value="メニュー追加">
  </div>
</form>

自分で試したこと

Formsetを使用しないでinputで実装しようとしたんですが余計にわからなくなったので、諦めました。

参考にした記事

0

No Answers yet.

Your answer might help someone💌