Djangoで、複数のform要素を使う

Twitterでシェア FaceBookでシェア はてなブックマークでシェア

Python - Django
2018年12月1日19:20に更新(約12日前)
2018年12月1日10:48に作成(約12日前)

概要

Djangoで、複数のform要素を使う例を紹介します。

以下はYoutubeのコメント欄のスクリーンショットです。 コメント1つ1つに返信フォームもある

Youtubeではコメントの一覧があり、それへの返信を同ページ内で行えます。こういうUIは中々人気で、至るところで見かけます。ブログのコメントに対する返信なんかも、そのページ内で行えることがありますね。

今回はこのような機能を実装するために、簡単な日記帳を作成します。以下のような感じです。 シンプルなサンプル

models.py

あまり実用的とはいいがたいですが、シンプルにしました。日記があり、それに紐づくコメントがあります。

from django.db import models


class Diary(models.Model):
    text = models.TextField('日記')
    created_at = models.DateTimeField(auto_now_add=True)


class Comment(models.Model):
    text = models.CharField('コメント', max_length=300)
    target = models.ForeignKey(Diary, on_delete=models.CASCADE, verbose_name='紐づく日記')
    created_at = models.DateTimeField(auto_now_add=True)

forms.py

モデルフォームを使います。targetfieldsに含めないようにしておきましょう。それをすると紐づく日記というプルダウンが表示されてしまいます。

from django import forms
from .models import Comment


class CommentCreateForm(forms.ModelForm):

    class Meta:
        model = Comment
        fields = ('text',)

views.py

少し長くなりましたが、やっていることは単純です。

from django.views.decorators.http import require_POST
from django.shortcuts import render, redirect, get_object_or_404
from .forms import CommentCreateForm
from .models import Diary


def index(request):
    context = {
        'diary_list': Diary.objects.all(),
        'form': CommentCreateForm(),
    }
    return render(request, 'app/diary_list.html', context)


@require_POST
def create_comment(request, pk):
    form = CommentCreateForm(request.POST)
    if form.is_valid():
        comment = form.save(commit=False)
        comment.target = get_object_or_404(Diary, pk=pk)
        comment.save()
        return redirect('app:index')

    context = {
        'diary_list': Diary.objects.all(),
        'form': form,
    }
    return render(request, 'app/diary_list.html', context)

indexビューは、日記の一覧とコメント作成用フォームをテンプレートへ渡します。

create_commentビューは、コメントの送信で呼ばれるビューです。ビューには日記のpkが渡されるので、それをコメントに設定して保存をします。

base.html

Bootstrap4のスターターテンプレートです。

<!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.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">

    <title>一言日記</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.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
    {% block extrajs %}{% endblock %}
  </body>
</html>

diary_list.html

日記の一覧とコメントフォームのある、今回の主役テンプレート。

{% extends 'app/base.html' %}

{% block content %}
    <h1 class="mb-5">マイダイアリー</h1>
    {% for diary in diary_list %}
        {{ diary.text | linebreaksbr }} <span class="text-muted">{{ diary.created_at }}</span>

        <!-- コメント一覧と、コメントフォーム -->
        <div class="mt-3 ml-5">
            <!-- その日記へのコメントフォーム -->
            <form action="{% url 'app:comment_create' diary.pk %}" method="POST">
                {{ form }}
                <button type="submit">送信</button>
                {% csrf_token %}
            </form>

            <!-- その日記に紐づいたコメントを取り出す -->
            {% for comment in diary.comment_set.all %}
                {{ comment.text }} <span class="text-muted">{{ diary.created_at }}</span><br>
            {% endfor %}
        </div><!-- コメント一覧・コメントフォーム終わり -->

        <hr>
    {% endfor %}
{% endblock %}

日記を一つずつ取り出すのが{% for diary in diary_list %}の部分ですが、そのforループの中で<form>要素を定義してることに注意です。日記が2件ならform要素は2つ作られますし、10件なら10個作られます。

各フォームは<form action="/comment/4/" method="POST">のようになります。4の部分は日記のpkで、create_commentビューにpk引数として渡されるものです。

type="submit"なボタンは、あくまでそれが属するフォーム要素のデータだけを送信します。

バリデーション失敗時の注意点

1つ注意があるのは、フォームのヴァリデーションが上手くいかなかったときです。空にしてはいけない入力欄を空にした、等のケースですね。

ビューでのform.is_valid()でFalseとなり、エラーメッセージ付きのフォームがテンプレートへ再度渡されます。このとき、以下のように表示されてしまいます。 全てのフォームにエラーが表示される

全てのフォームにエラー内容が表示されてしまいますし、前回入力した内容が他の全てのフォームに入っています。同じフォームオブジェクトを使っているので仕方ないのですが、見た目的にはよろしくありません。

これを避ける場合、よくあるのはエラー内容を別ページで表示する方法があります。

別ページでエラーを表示する

まずビューを変更します。

@require_POST
def create_comment(request, pk):
    form = CommentCreateForm(request.POST)
    if form.is_valid():
        comment = form.save(commit=False)
        comment.target = get_object_or_404(Diary, pk=pk)
        comment.save()
        return redirect('app:index')

    # ここ以降は、フォームの入力内容がおかしい(is_valid()がFalse)場合だけ
    context = {
        'form': form,
    }
    return render(request, 'app/comment_error.html', context)

comment_error.htmlというテンプレートを使います。そのcomment_error.htmlではフォームのエラー内容を見せます。

{% extends 'app/base.html' %}

{% block content %}
    {{ form.errors }}
    <a href="{% url 'app:index' %}">戻る</a>
{% endblock %}

以下がエラー表示ページです。実際はもう少し見栄えを気にする必要があると思いますが、流れはなんとなくわかったと思います。 エラー内容を表示している

エラーの表示場所を変える

もう一つは、同じページを使うがエラーの表示場所を変える方法です。上のほうに1つだけ表示させてみました。 エラーの表示場所を変えた

まずビューを修正します。

@require_POST
def create_comment(request, pk):
    form = CommentCreateForm(request.POST)
    if form.is_valid():
        comment = form.save(commit=False)
        comment.target = get_object_or_404(Diary, pk=pk)
        comment.save()
        return redirect('app:index')

    context = {
        'diary_list': Diary.objects.all(),
        'error_form': form,
        'form': CommentCreateForm(),
    }
    return render(request, 'app/diary_list.html', context)

変更箇所は以下の部分です。

        'error_form': form,
        'form': CommentCreateForm(),

error_formという名前でis_valid()に失敗したフォームを渡し、formとして新しくフォームを渡します。

なぜこんなことをするかといいますと、formオブジェクトに前回入力した内容が入ってしまっているため、テンプレートへ渡した際、全てのフォームが入力済みになります。なので、{{ form }}として渡すものは新品のフォームを渡し、エラー内容が入ったフォームは別途{{ error_form }}として渡すのです。

diary_list.htmlを修正します。

{% extends 'app/base.html' %}

{% block content %}
    {{ error_form.errors }}
    <h1 class="mb-5">マイダイアリー</h1>
    {% for diary in diary_list %}
        {{ diary.text | linebreaksbr }} <span class="text-muted">{{ diary.created_at }}</span>

        <!-- コメント一覧と、コメントフォーム -->
        <div class="mt-3 ml-5">
            <!-- その日記へのコメントフォーム -->
            <form action="{% url 'app:comment_create' diary.pk %}" method="POST">
                {{ form }}
                <button type="submit">送信</button>
                {% csrf_token %}
            </form>

            <!-- その日記に紐づいたコメントを取り出す -->
            {% for comment in diary.comment_set.all %}
                {{ comment.text }} <span class="text-muted">{{ diary.created_at }}</span><br>
            {% endfor %}
        </div><!-- コメント一覧・コメントフォーム終わり -->

        <hr>
    {% endfor %}
{% endblock %}

{{ error_form.errors }}として、エラー内容を表示させます。

Twitterでシェア FaceBookでシェア はてなブックマークでシェア

記事にコメントする