【Django】inlineformsetを用いたFormsetの更新・削除機能の実装について
解決したいこと
inlineformset_factoryを使用したFormsetの更新・削除機能を実装したい
Djangoで飲食店のメニュー登録アプリを作成しています。
メニューの追加をする際にメニューに対してinlineformsetを使ってオプションを付ける機能を実装したのですが、
更新・削除機能がどうしてもうまく行かず苦戦しています。
実装した機能は「ラーメン」というメニューに対して、「大盛り」などのオプションを選択できるようにするような機能です。
解決方法がわかる方がいましたら教えて下さい。
発生している問題・エラー
メニューを作成する際に一緒にオプションを作成する時はうまくデータベースに反映されるのですが、
一度データベースに反映されたものを更新・削除する際に項目が倍になってしまいます。
例えば、ラーメンというメニューに対して大盛りのオプションを付けた後、特盛りに修正しようとすると、
大盛りと特盛りが2つ並んでしまいます。
問題・エラーが起きている画像
【メニュー追加画面】
【ラーメンを追加した後の修正画面】
ここで「大盛り:100円」を「特盛り:200」に修正すると、、、
大盛りと特盛りに、、、というか更新を押したら大盛りが2つになります。(削除をチェックして更新をしても大盛りが2つに)
解決方法がわかる方ぜひ教えていただきたいです。
該当するソースコード
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