フォームの内容を保持する(テンプレートファイルに変数を渡す方法)

2018-10-21 / PythonDjango

概要

Djangoで、会員登録機能を自作するシリーズの1つです。ユーザー情報の入力後に確認画面を表示したいと思います。どう実装するのかと言いますと、ユーザー情報入力後のデータが入ったformオブジェクトを、入力画面のビュー→確認画面のビュー→データ作成のビュー と渡していきます。今回のユーザー情報入力後の確認という例に限らず、フォームの内容を保持したいという場合には使えます。

トップページは、ユーザーの一覧が表示されています。

ユーザー情報入力画面で入力し、送信を押すと

確認画面で、内容を確認できます。

戻るで入力画面に戻ると、ユーザー名は既に入力済みの状態です。画像のように、パスワードも入力済みにして戻らせることもできます。

送信では、データが正しく追加されます。

urls.py

from django.urls import path
from . import views

app_name = 'register'

urlpatterns = [
    path('', views.UserList.as_view(), name='user_list'),
    path('user_data_input/', views.UserDataInput.as_view(), name='user_data_input'),
    path('user_data_confirm/', views.UserDataConfirm.as_view(), name='user_data_confirm'),
    path('user_data_create/', views.UserDataCreate.as_view(), name='user_data_create'),
]

forms.py

UserCreationFormを継承した、ユーザー作成用の一般的で、汎用的なフォームです。Bootstrap4対応しています。

from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import get_user_model

User = get_user_model()  # Userモデルの柔軟な取得方法


class UserCreateForm(UserCreationForm):
    """ユーザー登録用フォーム"""

    class Meta:
        model = User
        fields = (User.USERNAME_FIELD,)  # ユーザー名として扱っているフィールドだけ、作成時に入力する

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

views.py

ドックストリングで解説しているとおりです。 FormViewはCreateViewからデータ作成処理を抜いたようなもので、POSTで飛んできたデータをフォームに束縛してくれるため、こういう時は便利です。

from django.contrib.auth import get_user_model
from django.shortcuts import render
from django.urls import reverse_lazy
from django.views import generic
from .forms import UserCreateForm

User = get_user_model()


class UserList(generic.ListView):
    template_name = 'register/user_list.html'  # デフォルトUserだと、authアプリケーションのuser_list.htmlを探すため、明示的に書いておく
    model = User


class UserDataInput(generic.FormView):
    """ユーザー情報の入力

    このビューが呼ばれるのは、以下の2箇所です。
    ・初回の入力欄表示(aタグでの遷移)
    ・確認画面から戻るを押した場合(これはPOSTで飛んできます)

    初回の入力欄表示の際は、空のフォームをuser_data_input.htmlに渡し、
    戻る場合は、POSTで飛んできたフォームデータをそのままuser_data_input.htmlに渡します。

    """
    template_name = 'register/user_data_input.html'
    form_class = UserCreateForm

    def form_valid(self, form):
        return render(self.request, 'register/user_data_input.html', {'form': form})


class UserDataConfirm(generic.FormView):
    """ユーザー情報の確認

    ユーザー情報入力後、「送信」を押すとこのビューが呼ばれます。(user_data_input.htmlのform action属性がこのビュー)
    データが問題なければuser_data_confirm.html(確認ページ)を、入力内容に不備があればuser_data_input.html(入力ページ)に
    フォームデータを渡します。

    """
    form_class = UserCreateForm

    def form_valid(self, form):
        return render(self.request, 'register/user_data_confirm.html', {'form': form})

    def form_invalid(self, form):
        return render(self.request, 'register/user_data_input.html', {'form': form})


class UserDataCreate(generic.CreateView):
    """ユーザーデータの登録ビュー。ここ以外では、CreateViewを使わないでください"""
    form_class = UserCreateForm
    success_url = reverse_lazy('register:user_list')

    def form_invalid(self, form):
        """基本的にはここに飛んでこないはずです。UserDataConfrimでバリデーションは済んでるため"""
        return render(self.request, 'register/user_data_input.html', {'form': form})

base.html

Bootstrap4を使った、よくある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.1.0/css/bootstrap.min.css"
          integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous">

    <title>会員登録サンプル</title>
</head>
<body>

    <div class="container mt-3">
        {% 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.0/umd/popper.min.js"
            integrity="sha384-cs/chFZiN24E4KMATLdqdvsezGxaGsi4hLGOzlXwp5UZB1LY//20VyM2taTB4QvJ"
            crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js"
            integrity="sha384-uefMccjFJAIv6A+rW+L4AHf99KvxDjWSu1z9VI8SKNVmz4sk7buKt/6v9KI65qnm"
            crossorigin="anonymous"></script>
</body>
</html>

user_list.html

ユーザー作成ページへのリンクと、ユーザーの一覧が表示されるページ。

{% extends "register/base.html" %}
{% block content %}
<p><a href="{% url 'register:user_data_input' %}">ユーザー作成</a></p><hr>
{% for user in user_list %}
    <p>ユーザー名: {{ user.username }}</p><hr>
{% endfor %}
{% endblock %}

user_data_input.html

ユーザー情報の入力ページ。formのaction属性が、{% url 'register:user_data_confirm' %}となっていることに注意してください。よくある、action=""ではないです。

{% extends "register/base.html" %}
{% block content %}
<form action="{% url 'register:user_data_confirm' %}" method="POST">
    {{ form.non_field_errors }}
    {% for field in form %}
    <div class="form-group">
        <label for="{{ field.id_for_label }}">{{ field.label_tag }}</label>
        {{ field }}
        {{ field.errors }}
    </div>
    {% endfor %}
    {% csrf_token %}
    <button type="submit" class="btn btn-primary btn-lg">送信</button>
</form>
{% endblock %}

user_data_confirm.html

確認ページです。

{% extends "register/base.html" %}
{% block content %}
    {% for field in form %}
    <div class="form-group">
        <label for="{{ field.id_for_label }}">{{ field.label_tag }}</label>
        {{ field.value }}
    </div>
    {% endfor %}

    <form action="{% url 'register:user_data_input' %}" method="POST">
        <button type="submit" class="btn btn-primary btn-lg">戻る</button>
        {% for field in form %}{{ field.as_hidden }}{% endfor %}
        {% csrf_token %}
    </form>
    <hr>
    <form action="{% url 'register:user_data_create' %}" method="POST">
        <button type="submit" class="btn btn-primary btn-lg">送信</button>
        {% for field in form %}{{ field.as_hidden }}{% endfor %}
        {% csrf_token %}
    </form>
{% endblock %}

ここは、入力された情報を表示するための部分です。{{ field.value }}で、値やテキストだけ取り出すことができます。これはinput要素を生成しない、単純なテキスト表示です。生のパスワードが表示されて気持ち悪い場合は、各フィールドを直接書いていくとよいでしょう。

    {% for field in form %}
    <div class="form-group">
        <label for="{{ field.id_for_label }}">{{ field.label_tag }}</label>
        {{ field.value }}
    </div>
    {% endfor %}

そして、戻る部分です。入力内容をGETメソッドで送るのはアレなので、POSTで送信します。 POSTで送信するためにはinput type=...のような要素が必要ですが、それを表示する訳にはいきません(見た目的な問題です。やってみるとわかります)。 幸いにも、{{ field.as_hidden }} でinput type="hidden"な表示されない要素にすることができるので、それをしています。

    <form action="{% url 'register:user_data_input' %}" method="POST">
        <button type="submit" class="btn btn-primary btn-lg">戻る</button>
        {% for field in form %}{{ field.as_hidden }}{% endfor %}
        {% csrf_token %}
    </form>

送信ボタンも同じで、formのaction属性がuser_data_createになる以外は、戻るボタンと同じです。

    <form action="{% url 'register:user_data_create' %}" method="POST">
        <button type="submit" class="btn btn-primary btn-lg">送信</button>
        {% for field in form %}{{ field.as_hidden }}{% endfor %}
        {% csrf_token %}
    </form>

参考としてお見せすると、as_hiddenで隠している部分は以下のような感じになります。

<input type="hidden" name="username" value="toritoritorina@gmail.com" id="id_username" />
<input type="hidden" name="password1" value="hello12345" id="id_password1" />
<input type="hidden" name="password2" value="hello12345" id="id_password2" />

処理の流れ

流れとしては、以下のようになります。

UserDataInputビュー

user_data_input.html(入力画面)
↓(送信ボタンを押す)
UserDataConfirmビュー
↓(入力値に問題がなければ)
user_data_confirm.html(確認画面)

この後、「戻る」を押すとUserDataInputビューに戻ります。 「送信」を押すとUserDataCreateビューへ行き、データを作成後一覧画面へ戻ります。

戻った際に、パスワードも入力済みにしたい場合

forms.pyの__init__内を、以下のようします。widget.render_value = Trueで、input type="password"の場合でも入力欄がクリアされません。

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

この記事の関連記事

Djangoで会員登録機能を自作するシリーズ

2018-10-19 / PythonDjangoシリーズ・まとめ

- Djangoで会員登録機能を自作していきます。メールアドレスをユーザー名として使うようにし、ログイン画面、仮登録、メールクリックで本登録、ユーザー情報変更ページ、パスワード変更ページ、パスワードを忘れた際の再設定...などなど、よくある一連の機能を実装します。

Djangoで、フォームの内容を保持する(セッションを使う方法)

2018-10-22 / PythonDjango

- Djangoで、会員登録機能を自作するシリーズの1つです。ユーザー情報の入力後に確認画面を表示したいと思います。ユーザー情報が入ったPOSTデータをセッションに保存する方法を使いますが、中々に便利です。

コメント欄

記事にコメントする

名無し

少しこの記事と違うかもしれませんが、「カスタムバリデーションを実装し、バリデーションに引っかかった場合はフォームの内容を保持したまま送信ページに戻してやる」といった処理をユーザーがフォームを送信した際に行いたいのですが、それを実現するためにはどのようにすればいいのでしょうか? form_validを使って実現可能なのでしょうか?

コメントに返信する

なりと

現状でも、フォーム送信時に何らかの入力が不正だった場合は入力画面に戻ります。

なので、カスタムバリデーション部分だけ作成すれば大丈夫です。

カスタムバリデーションの方法は色々ありますが、今回ならばフォームのclean()メソッドやclean_フィールド名()メソッドの上書きを試してください。

https://docs.djangoproject.com/ja/2.2/ref/forms/validation/

名無し

コメント失礼します。

記事の内容を参考に、作成中のアプリケーションで確認画面を作成しましたが、「戻る」ボタンを押すと空のフォームとなってしまい、うまく動きませんでした…

そこで記事掲載のソースコードをそのまま写して、registerアプリケーションを作成しました。「戻る」ボタンを押すと、ユーザーネーム入力欄のみ内容が保持されており、こちらも記事の通り動かず…

Djangoのバージョンやブラウザの問題なのでしょうか…? ご回答よろしくお願いいたします。

コメントに返信する

なりと

パスワードの内容を保持したまま前に戻りたい場合は、「戻った際に、パスワードも入力済みにしたい場合」の部分を試してください。

それ以外の部分も保持されない場合、ソースコードの内容を教えてください。

名無し

いつも記事を参考に学習させていただいています。一点教えていただきたく、質問させていただきました。 ブログのwebアプリを作っております。 個別の記事を表示するページ(article.html)と、キーワードで記事を検索するページ(find.html)があり、個別の記事のページに設置した検索窓にキーワードを入力してsubmitすると、キーワードが検索ページの検索フォームに渡されて検索結果を表示する、ということを行いたいと思っていますが、うまく行きません。 具体的には article.htmlを

<body>
    <form action="{% url 'blog:find' %}" method="post">{% csrf_token %}
    検索:<input type="text" name="kensaku">
    <input type="submit" value="検索">
</boy>

のようにし、find.htmlを

<body>
    <p>検索語を入力</p>
    <table>
        <form action="{% url 'blog:find' %}" method="post">{% csrf_token %}
        {{form}}
        <tr><th></th><td><input type="submit" value="検索"></td></tr>
        </form>
    </table>
    <hr>
    <table>
        <tr>
            <th>{{message|safe}}</th>
            <th></th>
            <th></th>
        </tr>
    {% for item in data_and %}
        <tr>
            <td><a href="{% url 'blog:text_article' item.id %}">{{item}}</a></td>
        <tr>
    {% endfor %}
</body>

のようにしました。 view関数は

def find(request):
    if (request.method == 'POST'):
        msg = 'search result:'
        form = FindForm(request.POST)
        str = request.POST['find']
        data = Post.objects.filter(name__contains=str)
    else:
        msg = 'search words...'
        form = FindForm()
        data =Post.objects.all()
    params = {
        'message': msg,
        'form':form,
        'data':data,
    }
    return render(request, 'blog/find.html', params)

のように記述して、ここから何とか上記のようなことを行えないかと考えました。 色々試行錯誤してみましたが、ど うにも上手くいかず質問させていただいた次第です。お時間がある時で結構ですので、ご教示いただけたら幸いです。

コメントに返信する

なりと

forms.pyの内容を教えてください。おそらく、input要素のname属性とフォームのフィールド名が一致していません。

名無し

お世話になります。form.pyの内容は

from django import forms

class FindForm(forms.Form): find = forms.CharField(label='検索語', required=False)

のようになっております。

なりと

article.htmlのinput要素を、次のように変更してください。nameをfindにします。

検索:<input type="text" name="find">
名無し

ありがとうございます。教えていただいたようにして上手くいきました! name属性に関する理解がなっていなかったようです。 これからもこちらのブログを参考に勉強させていただきます。