Django Model FormSet について

Djangoの標準機能であるModel FormSetを使ってみましたので、その備忘録です。FormSetは1画面に複数のフォームを表示するものです。


# １．Model FormSetとは

まずはModel Formや単なるFormSetの復習・予習です。


## １－１．Model Form

Djangoではmodelが定義してある場合、そのmodelから簡単にformを生成してくれるヘルパークラスModelFormがあります。これによりmodelとformで2重にfieldを定義することを避けることができ、一貫した定義が可能となります。

[モデルからフォームを作成する](https://docs.djangoproject.com/ja/2.2/topics/forms/modelforms/)


```
from django.db import models
from django.forms import ModelForm

TITLE_CHOICES = [
    ('MR', 'Mr.'),
    ('MRS', 'Mrs.'),
    ('MS', 'Ms.'),
]

class Author(models.Model):
    name = models.CharField(max_length=100)
    title = models.CharField(max_length=3, choices=TITLE_CHOICES)
    birth_date = models.DateField(blank=True, null=True)

    def __str__(self):
        return self.name

class Book(models.Model):
    name = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)

class AuthorForm(ModelForm):
    class Meta:
        model = Author
        fields = ['name', 'title', 'birth_date']

class BookForm(ModelForm):
    class Meta:
        model = Book
        fields = ['name', 'authors']
```



## １－２．FormSet

フォームセットとは、同じページで複数のフォームを扱うための抽象化レイヤで、いわばデータグリッドのようなものです。つまり一枚の画面で、複数のエントリーフォームが定義でき、複数の投稿が可能となります。

[フォームセット (Formset)](https://docs.djangoproject.com/ja/2.2/topics/forms/formsets/)


```
>>> from django import forms
>>> class ArticleForm(forms.Form):
...     title = forms.CharField()
...     pub_date = forms.DateField()
```


```
>>> from django.forms import formset_factory
>>> ArticleFormSet = formset_factory(ArticleForm)
```


## １－３．Model FormSet

通常のフォームセット のように、Django には Django モデルを簡単に扱うための拡張的なフォームセットのクラスが用意されています。

[モデルのフォームセット]
(https://docs.djangoproject.com/ja/2.2/topics/forms/modelforms/#using-the-formset-in-the-template)


```
>>> from django.forms import modelformset_factory
>>> from myapp.models import Author
>>> AuthorFormSet = modelformset_factory(Author, fields=('name', 'title'))
```


# ２．Model FormSetの実例


## ２－１．プロジェクト作成

まずはDjangoのプロジェクトを作成します。

```
python -m venv formset
source formset/bin/activate
cd formset
pip freeze   # インストール済みは空であることを確認
pip install django  # Djangoをインストール
django-admin startproject formset
cd formset
```

Remoteからアクセスするための設定です。

```python:statictest/settings.py
---
ALLOWED_HOSTS = ["www.mypress.jp"]
---
```

ここまででプロジェクトが動作するので確認します。

```
python manage.py migrate
python manage.py runserver 0:8080
```

テスト用のアプリを作成します

```
python manage.py startapp myapp
```


```
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'myapp',
]
```



## ２－２．画面イメージ

1行が1フォームです。5行あるので5フォーム表示されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61915/ae50a529-1b39-cf5e-4260-258751f97c8e.png)


## ２－３．ソースコード

```python:formset/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('myapp.urls')),
]
```


```python:myapp/urls.py
from django.urls import path
from . import views

app_name = 'myapp'

urlpatterns = [
    path('', views.addPost, name='index'),
]
```


```python:myapp/models.py
from django.db import models
from django.utils import timezone

class Post(models.Model):
    title1 = models.CharField('タイトル1', blank=True, default='', max_length=200)
    title2 = models.CharField('タイトル2', blank=True, default='', max_length=200)
    title3 = models.CharField('タイトル3', blank=True, default='', max_length=200)
    title4 = models.CharField('タイトル4', blank=True, default='', max_length=200)

    def __str__(self):
        return self.title1
```

**modelformset_factory()**では、与えられた model クラスに対して、FormSet クラスを返します。さらにここではextra=1を指定していますので新規投稿フォームが1個表示されます。max_num=5なので5個投稿すると新規投稿フォームは表示されなくなります。 

[モデルフォーム関数](https://docs.djangoproject.com/ja/2.2/ref/forms/models/)



```python:myapp/forms.py
from django import forms
from .models import Post

class PostCreateForm(forms.ModelForm):

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

    class Meta:
        model = Post
        fields = '__all__'


### modelformset_factoryはFormSetクラスを返します
PostCreateFormSet = forms.modelformset_factory(
    Post, form=PostCreateForm, extra=1, max_num=5
)
```


post requestの場合は、**formset.save()**で保存してredirectします。


```python:myapp/views.py
from django.shortcuts import render, redirect
from .forms import PostCreateFormSet


def addPost(request):
    ### formSetクラスのインスタンス
    formset = PostCreateFormSet(request.POST or None)
    print(formset.errors)
    if request.method == 'POST' and formset.is_valid():
        formset.save()
        return redirect('myapp:index')

    context = {
        'formset': formset  ### テンプレートにformsetを渡す
    }

    return render(request, 'myapp/formset.html', context)
```



Bootstrapの設定をbase.htmlで行います。公式サイトのものをコピペします。
[Starter template - Bootstrap](https://getbootstrap.com/docs/4.3/getting-started/introduction/)



```html:myapp/templates/myapp/base.html
<!doctype html>
<html lang="ja">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

    <title>Django Model FormSet について</title>
  </head>
  <body>
    <div class="container mt-5">
        {% block content %}{% endblock %}
    </div>

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
  </body>
</html>
```

**{{ formset.management_form }}** は、深い意味は知りませんが、formsetに必要です。
また**{{ form.id }}**を手動で追加していることに注意してください。idを明記しないとupdate時にエラーになります。

```html:myapp/templates/myapp/formset.html
{% extends 'myapp/base.html' %}

{% block content %}
<form action="" method="post">
    {% for form in formset %}
        {{ form.id }}
        <div class="row">
            <div class="col-sm-3">
                {{ form.title1 }}
                {{ form.title1.errors }}
            </div>
            <div class="col-sm-3">
                {{ form.title2 }}
                {{ form.title2.errors }}
            </div>
            <div class="col-sm-3">
                {{ form.title3 }}
                {{ form.title3.errors }}
            </div>
            <div class="col-sm-3">
                {{ form.title4 }}
                {{ form.title4.errors }}
            </div>
        </div>
    {% endfor %}
    {{ formset.management_form }}
    {% csrf_token %}
    <button type="submit" class="btn btn-primary">送信</button>
</form>
{% endblock %}
```


今回は以上です