naritoブログ

【お知らせ】
・コメントで質問等をしたが返事が返ってこない場合、私はそれを見落としています。
その場合は再度コメントをするかメールをしてください(toritoritorina@gmail.com)。

・近いうちに新しいブログが作成されます。わーお!

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

約4日前 2018年9月19日18:14
プログラミング関連
Django Python

概要


Djangoで、ユーザー情報の入力後に確認画面を表示したいと思います。

ユーザー情報が入ったPOSTデータをセッションに保存する方法を使いますが、中々に便利です。

見た目


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

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

確認画面で、内容を確認できます。
送信を押すと確認画面へ。さきほど入力した内容が移されている

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

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

ソースコード



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.user_data_input, name='user_data_input'),
    path('user_data_confirm/', views.user_data_confirm, name='user_data_confirm'),
    path('user_data_create/', views.user_data_create, 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


解説は、コメントのとおりです。
処理がやや複雑なので、クラスビューではなく関数ビューで作成しました。
from django.contrib.auth import get_user_model
from django.shortcuts import render, redirect
from django.views import generic
from django.views.decorators.http import require_POST
from .forms import UserCreateForm

User = get_user_model()


class UserList(generic.ListView):
    """ユーザーを一覧表示。"""
    # デフォルトUserだと、authアプリケーションのuser_list.htmlを探すため、明示的に指定する。
    template_name = 'register/user_list.html'
    model = User


def user_data_input(request):
    """新規ユーザー情報の入力。"""
    # 一覧表示からの遷移や、確認画面から戻るリンクを押したときはここ。
    if request.method == 'GET':
        # セッションに入力途中のデータがあればそれを使う。
        form = UserCreateForm(request.session.get('form_data'))
    else:
        form = UserCreateForm(request.POST)
        if form.is_valid():
            # 入力後の送信ボタンでここ。セッションに入力データを格納する。
            request.session['form_data'] = request.POST
            return redirect('register:user_data_confirm')

    context = {
        'form': form
    }
    return render(request, 'register/user_data_input.html', context)


def user_data_confirm(request):
    """入力データの確認画面。"""
    # user_data_inputで入力したユーザー情報をセッションから取り出す。
    session_form_data = request.session.get('form_data')
    if session_form_data is None:
        # セッション切れや、セッションが空でURL直接入力したら入力画面にリダイレクト。
        return redirect('register:user_data_input')

    context = {
        'form': UserCreateForm(session_form_data)
    }
    return render(request, 'register/user_data_confirm.html', context)


@require_POST
def user_data_create(request):
    """ユーザーを作成する。"""
    # user_data_inputで入力したユーザー情報をセッションから取り出す。
    # ユーザー作成後は、セッションを空にしたいのでpopメソッドで取り出す。
    session_form_data = request.session.pop('form_data', None)
    if session_form_data is None:
        # ここにはPOSTメソッドで、かつセッションに入力データがなかった場合だけ。
        # セッション切れや、不正なアクセス対策。
        return redirect('register:user_data_input')

    form = UserCreateForm(session_form_data)
    if form.is_valid():
        form.save()
        return redirect('register:user_list')

    # is_validに通過したデータだけセッションに格納しているので、ここ以降の処理は基本的には通らない。
    context = {
        'form': form
    }
    return render(request, 'register/user_data_input.html', context)




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.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" 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.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>
  </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


ユーザー情報の入力ページ
{% extends "register/base.html" %}
{% block content %}
<form action="" 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 %}

    <a href="{% url 'register:user_data_input' %}" class="btn btn-primary btn-lg">戻る</a>
    <hr>
    <form action="{% url 'register:user_data_create' %}" method="POST">
        <button type="submit" class="btn btn-primary btn-lg">送信</button>
        {% 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 %}



以下は戻るボタンです。入力内容はセッションに保存済みなので、単純にアンカータグで戻れます。
 <a href="{% url 'register:user_data_input' %}" class="btn btn-primary btn-lg">戻る</a>


送信はPOSTで送信しますが、入力内容をセッションに保存しているので、入力欄などは特に必要ありません。 {% csrf_token %}とsubmitなボタンだけで大丈夫です。
    <form action="{% url 'register:user_data_create' %}" method="POST">
        <button type="submit" class="btn btn-primary btn-lg">送信</button>
        {% csrf_token %}
    </form>



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


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'
名無し 約83日前 2018年7月2日18:41 返信する
ユーザーが記事を投稿できるアプリで、ユーザーがタグをつけて投稿できるようにしています。
しかし、テストしてみるとユーザーが選択したはずのタグが管理画面に全く反映されていません。
管理者ユーザーで管理画面から記事を追加すればきちんと反映されます。
これは何か不具合が起きているのでしょうか?
タグはManyToManyでArticle(記事のオブジェクト)と紐づけています。
なりと 約83日前 2018年7月2日20:47
記事投稿のビューが上手く動作していないように思います。
ソースコードを見せていただけますか。
名無し 約83日前 2018年7月2日21:49 返信する
こちらがviews.pyのソースコードです
class ArticleCreateView(LoginRequiredMixin, CreateView):
model = Article
form_class = ArticleForm
template_name = "maps/add_article.html"
success_url = "/user" # 成功時にリダイレクトするURL

def form_valid(self, form):
article = form.save(commit=False)
current_user = self.request.user
article.user = current_user
marker.save()
return redirect("home")
名無し 約83日前 2018年7月2日22:22
すみません。
下から2行目のmarkerはタイポで、articleです。
なりと 約83日前 2018年7月3日9:55
models.py、forms.pyとadd_article.htmlの内容も教えていただけますか。
名無し 約83日前 2018年7月3日10:39 返信する
添付ファイルダウンロード(models.py.txt)
中身はこちらです。
forms.py----------------------------------

from django import forms
from .models import Article
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = ("name", "body", "category", "tag")

add_article.html--------------------------
<!DOCTYPE html>
<html lang = "ja-jp">
<head>
{% load static %}
<meta charset="utf-8">
.
.
.
</head>
<body>

<div class="container">
<form class="form_container" method="post" enctype="multipart/form-data">
<table>
{% for field in form %}
<tr class="warning">
<td>{{ field.label_tag }}</td>
<td>{{ field }}</td>
</tr>
{% endfor %}
</table>
{% csrf_token %}
<button type="submit">post</button>
</form>
</div>

</body>
</html>
なりと 約82日前 2018年7月3日14:06
私もすっかり忘れていました。
今回の例のようにManyToManyを含むフィールドがあり、かつsave(commit=False)を使う場合は1つ呼び出すメソッドが増えます。article.save()のあとにform.save_m2m()という呼び出しを追加してください。

https://docs.djangoproject.com/ja/2.0/topics/forms/modelforms/#the-save-method
ドキュメントの内容そのままを紹介します。

commit=False を使う際のもう 1 つの副作用は、モデルに他のモデルとの多対多の関係がある場合に見られます。フォームを save するときにモデルに多対多の関係があり commit=False を指定した場合、Django は多対多の関係に対してフォームのデータを即座に保存することができません。
...
...
save_m2m() の呼び出しは、save(commit=False) を使った場合にのみ必要となります。
名無し 約82日前 2018年7月3日14:22 返信する
上手くいきました、ありがとうございます。
勉強になります。
名無し 約65日前 2018年7月20日16:24 返信する
初めまして、最近Djangoをし始めたものです。
いろんなバリエーションについての解説があり、非常に参考にさせていただいています。

さて、本記事についてご質問があります。
views.pyについて、下記メソッドがありますが、
・get_form()
・form_valid()

これはどういう働きをして、なぜ利用されているのでしょうか??
Djnagoは準備されているメソッドが多いわりに日本語の解説が少なく、なかなか理解が進みません。

どうぞご教授いただけると幸いです。宜しく申し上げます。
なりと 約64日前 2018年7月21日18:39
ユーザーが入力したデータを長く保持する必要があったので、セッションを利用しています。
form_validは入力データ(request.POST)が問題ない場合(空の値がない、文字数をオーバーしていない、etc)に呼ばれるメソッドで、入力データが正常なときだけセッションに入力データを格納しています。
今回のような使い方は少し珍しく、よくあるのは入力データが問題なければメールを送信するとか、ログイン中のユーザーをモデルインスタンスに設定するために上書きするとか、そういった使い方が多いです。

このform_validでセッションに格納した入力データを、get_formメソッドで取り出しています。
FormViewやCreateView、UpdateViewにあるget_formメソッドはフォームインスタンスを返すためのメソッドで、元々の処理は大雑把に説明しますと
return UserCreateForm(self.request.POST)
といったことしています。

今回はget_formメソッドを上書きしていますが、大雑把に説明すると以下のような処理をしています。
return UserCreateForm(self.request.session.get('form_data', None))
セッションにはrequest.POSTを保存しているので、もう少し噛み砕くと以下のような処理です。
return UserCreateForm(入力画面で送信されたself.request.POST)

UserDataConfirmはgeneric.TemplateViewを継承していますが、TemplateViewにはget_formメソッドがないのでget_context_dataメソッド内で同様の処理をしています。


処理の流れがわかりにくいので、そのうち関数ビューで書き直すと思います。
名無し 約64日前 2018年7月21日22:30 返信する
ご丁寧に解説いただき誠にありがとうございます。

おかげさまで、全体の流れが読めるようになりました。
その上で、最後に2点だけお伺いしたいのですが、

1)CreateViewでもget_context_data()は使えるようですが、同じことができるのあ
れば、どういった理由からget_context_data()ではなくget_form()を利用されたのでしょうか?? こちらでも色々といじっていたところ、UserDataCreateでget_form()を利用すると、modelやtemplate_nameを利用しなくても問題なく処理が実行され、逆にmodelやtemplate_nameを設定すると(get_formはコメントアウト)、CreateViewクラス由来のフォームが生成されて、思うような動きにならないことが分かりました。(細かくて申し訳ございません、、。)

2)Django全体の実装方法の考え方として【Djangoでは各クラスで実装されているメソッドが然るべきタイミングで実行されるので、そのタイミングで任意の処理をしたければ、そのメソッドをオーバライドする】というように解釈をしたのですが、この解釈の仕方で大丈夫でしょうか。

お忙しいなか大変恐縮ですが、最後に上記2点だけご教授いただけると幸いです。
それではどうぞ宜しくお願い致します。
なりと 約64日前 2018年7月22日0:39
FormViewやCreateView、UpdateViewではget_formを呼び出す箇所が2箇所あるため、get_context_dataメソッドの上書きでは上手くいかないというのが理由です。

1つはGETメソッドでのリクエスト時(URL直接アクセスや、リンクを踏んだとき)で、これはクラスベースビューのgetというメソッドが呼ばれます。getメソッド内では
context = self.get_context_data()
としており、get_context_dataメソッドでは
'form': self.get_form(),
という処理をしています。なので、GETメソッドでのリクエスト時はget_context_dataメソッドだけ上書きしても動作します。

しかし、POSTメソッドでのリクエスト...<form method='POST'>内でsubmitなボタンを押した場合は、クラスベースビューのpostというメソッドが呼ばれます。postメソッド内では、
form = self.get_form()
 if form.is_valid():
  return self.form_valid(form)
 else:
  return self.form_invalid(form)
といった処理をしており、こちらでもget_formメソッドを呼んでフォームオブジェクトを取得します。この後にis_validメソッドで入力値の検査を行うという流れです。POSTに対応するためにはget_context_dataメソッドではなくget_formメソッドの上書きが必要でした。

TemplateViewではそもそもget_formメソッドは定義されておらず、また呼び出される箇所もないので、get_context_dataを上書きするしかありませんでした。(TemplateViewでは通常postメソッドは呼ばれないので、そちらは考えなくて大丈夫です)

2) について、その認識で問題ございません。タイミングの都合と、もう一つ重要なこととして
クラスベースビューのメソッドにはそれぞれ役割がありますので、慣れるとこういう処理はこのメソッド内でやるべきだな、というのが分かるようになります。
テンプレートへ渡す変数を増やしたければ、テンプレートへ渡す変数を作るためのメソッドであるget_context_dataを上書きしようだとか、ListViewで一覧表示するデータをカスタマイズするならば、データの一覧ををDBから取得するquerysetメソッドを上書きしよう、といった具合です。
名無し 約64日前 2018年7月22日0:50 返信する
遅い時間までありがとうございます。

大変良くわかりました。
不明点が解決され、色々と繋がりが見えて来ました。

大大大感謝です。
ありがとうございます。mm
名無し 約48日前 2018年8月6日11:32 返信する
こんにちは、コメント欄の作り方について質問させていただきます。
Youtubeのコメント送信フォームのようなものを実装しようと考え、ModelFormとCreateViewを利用するつもりでいたのですが、その際に普通のviewのように、テンプレートに変数を渡して表示させる方法がわかりません。
CreateViewではフォームのみを持つページしか作れず、ユーザー情報をテンプレートに渡して表示させるようなことはできないのでしょうか?
名無し 約48日前 2018年8月6日16:44
追記です。
get_context_data()をオーバーライドする方法でテンプレートに値を渡すことには成功しました。
しかし今度は、作ったコメントフォームからコメントを送信しようとすると下記のエラーが出てしまいました。

duplicate key value violates unique constraint "〇〇〇_slug_key"
DETAIL: Key (slug)=() already exists.

一回目のコメント送信は問題なくできたのですが、同じユーザーで二回目以降のコメント送信をしようとするとこのエラーが出てしまうようです。
と、ここまでは調べがついたのですが、slugについての知識が全くないのでどう解決していいかが分かりません。
もしご存知なら教えていただけませんか?
なりと 約47日前 2018年8月7日23:59
モデルとビューがどうなっているのか見たいです。
名無し 約46日前 2018年8月8日20:21 返信する
views.pyです

class CommentCreateView(LoginRequiredMixin, CreateView):
model = SNSComment
form_class = SNSCommentForm
template_name = "maps/sns_detail.html"
success_url = "/user" # 成功時にリダイレクトするURL

def form_valid(self, form):
comment = form.save(commit=False)
current_user = self.request.user
comment.user = current_user
pk = self.request.POST["pk"]
comment.relatedmarker = Marker.objects.get(pk=pk)
comment.save()
form.save_m2m()
return redirect("home")

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
pk = self.request.GET.get("pk")
marker = Marker.objects.get(pk=pk)
comments = SNSComment.objects.filter(relatedmarker=marker)

context["marker"] = marker
context["pk"] = pk
return context
名無し 約46日前 2018年8月8日20:24 返信する
models.pyです

class SNSComment(models.Model):
slug = models.SlugField(unique=True, blank=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET(make_and_set))
title = models.CharField(max_length=30)
body = models.CharField(max_length=3000)
relatedmarker = models.ForeignKey(Marker, on_delete=models.CASCADE)
image = models.ImageField(upload_to="media/marker/commentimage/{}/".format(relatedmarker.name), blank=True)
like = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, related_name='comment_likes')
published = models.DateTimeField(auto_now_add=True, null=True)


def __str__(self):
return self.title

def get_absolute_url(self):
return reverse("snscomment:", kwargs={"slug": self.slug})
なりと 約45日前 2018年8月9日20:36
slug = models.SlugField(unique=True, blank=True)
としていますが、form_validメソッドを見るとslugを設定していないような気がします。
blank=Trueなので空でも保存はできるのですが、unique=Trueとしているので重複が許されません。slugを空で再度登録しようとすると、重複が許されないということでエラーになっている気がします。

slugフィールドの値をform_valid内で設定するようにしてみてください。
まだエラーが出る場合は、プロジェクトを添付していただけると助かります。
名無し 約43日前 2018年8月11日12:38 返信する
なりとさんの指摘通り、slugfieldの設定と実際の運用がかみ合っていませんでした。
signalを使って自身のpkをslugに設定することで解決できました。
困っていたので本当に助かりました。ありがとうございます。
ブログでdjangoの解説記事を書いて、コメント欄で質問に答えながら補足していくというこの手法は、素晴らしいと思います。
コメント欄で記事では理解しきれないことをどんどん補って、このブログがパワーアップしていくような感覚です。(中二っぽくてすみません 笑)