Python + Django で、１対多モデルの登録画面を作成する

django-extra-viewsを使って、１対多モデルの登録画面を実装します。

###1.目的
データを入力するケースでは、単純なモデルだけを登録するようなケースはあまりなく、実践では１対多のリレーションをもつモデルを登録するケースが多いので、このようなケースでdjango-extra-viewsが便利です。

###2.環境

| カテゴリ            | バージョン         　 |
|:-------------------|:---------------------|
| OS                 |windows 10 home 64bit |
| python             |3.6.5                 |
| django-extra-views |0.11.0                |
| psycopg2           |2.7.4                 |


###3.セットアップ
####(1)django-extra-viewsをインストール
仮想環境で、pipを使ってインストールしましょう。

```cmd.prompt
(venv) C:\data\python\myproject>pip install django-extra-views
```
####(2)extra-viewsをプロジェクトに配置する
django-extra-viewsについては、以下のサイトを参考にしています。
https://github.com/AndrewIngram/django-extra-views
上記サイトを解凍して、「extra_views」フォルダをプロジェクトのフォルダの直下にコピーします

###4.アプリケーション作成
アプリケーションを作成していきます。
(1)startapp

```cmd.prompt
(venv) C:\data\python\myproject>python manage.py startapp extraviews_test
```
####(2)model
１対多モデルを作成します。Parentモデルに対して、子を３種類もつモデルを作成します。

```extraviews_test\models.py
from django.db import models

class Parent(models.Model):
    name = models.CharField(max_length=255)
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)


class Child1(models.Model):
    name = models.CharField(max_length=255)
    child1_column = models.CharField(max_length=255)
    Parent = models.ForeignKey(Parent, on_delete=models.CASCADE)

    def __unicode__(self):
        return self.name


class Child2(models.Model):
    name = models.CharField(max_length=255)
    child2_column = models.CharField(max_length=255)
    comments2 = models.CharField(max_length=255,null=True)
    Parent = models.ForeignKey(Parent, on_delete=models.CASCADE)

    def __unicode__(self):
        return self.name

class Child3(models.Model):
    name = models.CharField(max_length=255)
    child3_column = models.CharField(max_length=255)
    comments3 = models.CharField(max_length=255,null=True)
    Parent = models.ForeignKey(Parent, on_delete=models.CASCADE)

    def __unicode__(self):
        return self.name
```

####(3)settings.pyを編集
アプリケーションをプロジェクトに追加します。

```myproject\settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'extraviews_test', #追加
]
```

####(4)migrate
モデルをプロジェクトに追加します。

```cmd.prompt
(venv) C:\data\python\myproject>python manage.py makemigrations extraviews_test
```

データベースにモデルを登録します。

```cmd.prompt
(venv) C:\data\python\myproject>python manage.py migrate extraviews_test
```

####(5)extraviews_test\urls.py
アプリケーションレベルのurls.pyを追加します。startappでは作成されないのでファイルを作成する必要があります。
今回は、新規作成、更新のみとします。「success」は、新規、更新後に表示するだけのページです。

```extraviews_test\urls.py
from django.urls import path
from . import views

app_name = 'extraviews_test'
urlpatterns = [
    path('create/', views.ParentCreateView.as_view(), name='create'),
    path('update/<int:pk>/', views.ParentUpdateView.as_view(), name='update'),
    path('success/', views.SuccessView.as_view(), name='success'),
]
```

####(6)extraviews_test\views.py

viewもいたってsimpleです。

```
from django.urls import reverse_lazy
from extra_views import InlineFormSetFactory, CreateWithInlinesView, UpdateWithInlinesView
from django.views.generic import TemplateView
from .forms import ParentForm
from .models import Parent, Child1, Child2, Child3


class Child1Inline(InlineFormSetFactory):
    model = Child1
    fields = '__all__'
    


class Child2Inline(InlineFormSetFactory):
    model = Child2
    fields = '__all__'


class Child3Inline(InlineFormSetFactory):
    model = Child3
    fields = '__all__'


class ParentCreateView(CreateWithInlinesView):
    model = Parent
    fields = ['name']
    context_object_name = 'parent'
    inlines = [Child1Inline, Child2Inline, Child3Inline]
    template_name = 'extraviews_test/parent.html'
    success_url = reverse_lazy('extraviews_test:success')


class ParentUpdateView(UpdateWithInlinesView):
    model = Parent
    form_class = ParentForm
    inlines = [Child1Inline, Child2Inline, Child3Inline]
    template_name = 'extraviews_test/parent.html'
    success_url = reverse_lazy('extraviews_test:success')

class SuccessView(TemplateView):
    template_name = "extraviews_test/success.html"
```

####(7)extraviews_test\forms.py
forms.pyはstartappでは作成されていませんのでファイルを作成してください。

```extraviews_test\forms.py
from django import forms
from .models import Parent

class ParentForm(forms.ModelForm):
    class Meta:
        model = Parent
        fields = ['name']

    def save(self, commit=True):
        instance = super(ParentForm, self).save(commit=commit)

        if commit:
            instance.save()

        return instance
```

####(8)extraviews_test\templates\extraviews_test\parent.html

```extraviews_test\templates\extraviews_test\parent.html
{% extends "commons/base.html" %}

{% load static %}

{% block title %}
{% endblock title %}

{% block links %}
{% endblock links %}

{% block headertitle %}
ParentAndChilds Models（1対多）
{% endblock %}

{% block content %}

<form action="." method="post">
{% csrf_token %}

<p>親テーブル</p>
<table>
  {{ form.as_table }}
</table>

{% for formset in inlines %}
  <p>子テーブル(child{{forloop.counter}})</p>
  <table class="table table-sm ">
    {{ formset.management_form }}
    {% for form in formset %}
      <!-- ヘッダ出力 -->
      {% ifequal forloop.counter0 0 %}
        {% for field in form %}
        <td class="bg-light">
          {% if field.field.widget.is_hidden %}
          {% else %}
            {{ field.label_tag }}
          {% endif %}
        </td>
        {% endfor %}
      {% endifequal %}
      <tr>
        {% for field in form %}
        <td>
          {{ field }}
        </td>
        {% endfor %}
      </tr>
    {% endfor %}
  </table>

{% endfor %}

<input class="btn btn-primary" type="submit" value="登録" />

{% endblock content %}

{% block script %}
<script>
    $(function(){
    })
</script>
{% endblock %}
```
登録用のページは、サイトのサンプルを少しカスタマイズしています。

:point_up:Point
formsetは、以下のようにすることが簡単です、
{% for formset in inlines %}
  <table>
  </table>
{% endfor %}
ですが、formsetを上記のようにform→fieldに展開する場合は、「management_form」が必要になります。
{{ formset.management_form }}


####(9)extraviews_test\templates\extraviews_test\success.html

```extraviews_test\templates\extraviews_test\success.html
{% extends "commons/base.html" %}

{% load static %}

{% block headertitle %}
ParentAndChilds Models（1対多）
{% endblock %}

{% block content %}

<h1>Success</h1>

{% endblock content %}
```

###5.動作確認

(1)プロジェクトを実行

```cmd.prompt
(venv) C:\data\python\myproject>manage.py runserver
```

(2)URLにアクセス

新規登録
http://localhost:8000/extraviews_test/create/
![image.png](https://qiita-image-store.s3.amazonaws.com/0/246741/fd2681c8-aab1-9b3b-2601-8e87984dfd9e.png)

編集
http://localhost:8000/extraviews_test/update/1/
![image.png](https://qiita-image-store.s3.amazonaws.com/0/246741/84278e13-9591-50bb-a876-afdf62ff3ace.png)

子のモデルには、削除ボタンも表示されています。１対多モデルも簡単に実装できそうです。