nrRouteの日記

勉強において虚無精進をするブログです

Djangoでフォームの入力欄の追加をする方法(メモ)

DjangoのFormsetを使うときにフォームの追加をする方法をまとめておきます。メモなのでだいぶ雑です。

バージョンは3.2.20です。

Djangoのみでやる

こちらのサイトを参考にやっていたのですが、グローバル変数を使っており実用向きではありません。グローバル変数を使わずに書いてみます。

en-junior.com

models.py

from django.db import models

class Group(models.Model):
    name = models.CharField(max_length=100)

class Member(models.Model):
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    name = models.CharField(max_length=100)
    email = models.EmailField(max_length=255)

views.py

from django.views import generic
from django import forms
from django.shortcuts import render
from .models import Group, Member

class MemberView(generic.FormView):
    template_name = 'app/member.html'

    def get_success_url(self):
        return reverse_lazy('app:member', kwargs={'pk': self.kwargs['pk']})

    def get_form(self):
        group = Group.objects.get(pk=self.kwargs['pk'])
        MemberFormset = forms.modelformset_factory(
            Member,
            fields=["name", "email",],
            extra=3, max_num=100,
            can_delete=True,
        )
        queryset = Member.objects.filter(group=group)

        if self.request.method == 'POST':
            formset = MemberFormset(**self.get_form_kwargs())
        else:
            formset = MemberFormset(queryset=queryset)
        return formset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        return context

    def get_form_kwargs(self):
        kwargs = super.get_form_kwargs()
        if self.request.POST and 'add' in self.request.POST:
            kwargs['data'] = self.request.POST.copy()
            kwargs['data']['form-TOTAL_FORMS'] = int(kwargs['data']['form-TOTAL_FORMS']) + 1
        return kwargs

    def post(self, request, *args, **kwargs):
        if 'add' in request.POST:
            context = self.get_context_data(**kwargs)
            return render(request, self.template_name, context)
        return super().post(request, *args, **kwargs)

    def form_valid(self, form):
        if 'submit' in self.request.POST:
            data = form.cleaned_data
            group = Group.objects.get(pk=self.kwargs['pk'])

            for member_parameter in data:
                if member_parameter:
                    try:
                        member = member_parameter['id']
                        member.name = member_parameter['name']
                        member.email = member_parameter['email']
                        member.save()
                    except:
                        Member.objects.create(
                            group=group,
                            name=member_parameter['name'],
                            email=member_parameter['email'],
                        )
        return super().form_valid(form)

テンプレートには {{ form }} で描画できます。

JavaScriptを使ってやる

こちらのサイトを参考にやりました。JavaScriptはこちらのものを一部変更して、更新処理などをできるようにしました。

qiita.com

models.py(①と同じです)

from django.db import models

class Group(models.Model):
    name = models.CharField(max_length=100)

class Member(models.Model):
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    name = models.CharField(max_length=100)
    email = models.EmailField(max_length=255)

forms.py

from .models import Group, Member
from django import forms

MemberFormset = forms.inlineformset_factory(
    Group, Member, fields=('name', 'email'),
    extra=0, can_delete=True
)

views.py

from .forms import MemberFormset
from .models import Group, Member
from django.shortcuts import redirect, render, get_object_or_404

def member(request, pk):
    group = get_object_or_404(Group, pk=pk)
    formset = MemberFormset(request.POST or None, instance=group)

    if request.method == 'POST' and formset.is_valid():
        formset.save()
        return redirect('app:member', pk=group.pk)

    context = {
        'formset': formset,
        'group': group,
    }

    return render(request, 'app/member.html', context)

JavaScript

$(function() {

    // フォームの追加をする部分は同じなので省略します

    $('#form').submit(function() {

        const text = $('.text');
        $('[name=form-TOTAL_FORMS]').val(text.length);

        // それぞれの入力欄の__prefix__をindexで置換する
        text.each(function(index, element){
            originalHtml = $(element).html();
            replacedHtml = $(element).html().replace(/__prefix__/g, index);
            // empty_formのみ置換の処理を行う
            if (originalHtml !== replacedHtml) {
                // valueが消えるのですべての項目に対して保存
                value_name = $(element).find("#id_form-__prefix__-name").val();
                value_email = $(element).find("#id_form-__prefix__-email").val();
                is_delete = $(element).find("#id_form-__prefix__-DELETE").prop('checked');
                $(element).html(replacedHtml);
                $(element).find("#id_form-" + index + "-name").val(value_name);
                $(element).find("#id_form-" + index + "-email").val(value_email);
                $(element).find("#id_form-" + index + "-DELETE").prop('checked', is_delete);
            }
        });
    });
});

テンプレートにはすでに入力されている(されていない場合は空)formsetと、empty_formを1つ書いておきます。

ビューの実装はこちらも参考にしました。

blog.narito.ninja

2023/10/26追記: JavaScriptも一部変える必要があったので更新しました。