今回のテーマは「確認画面付きのトピック作成画面を作る」です。確認画面つき画面とはユーザーの入力した内容を一度表示して必要に応じて入力画面に戻ることができる画面のことです。アンケートページや申込みページでは頻繁に使われますね。これまで使ってきたCreateViewに少し手を加えるだけで簡単に確認画面つきの登録画面が作れますよ。
はじめにお断りしておくと、確認画面の作り方は色々な手法があって、今回ご紹介する方法はあくまで1つの例と捉えていただければと思います。データ保持にセッションを使ったり、URLをページ毎に分ける場合もありますし、ページ遷移はフロントのみ(バックエンドはAPIのみ担う)で対応するなど、ケースごとに対応が異なります。(これは確認画面に限った話ではないですが)
※本ページはFormViewとCreateViewを使うまで読まれた方を対象としています。そのためサンプルソースコードが省略されている場合があります。
確認事項
まず、現状を確認しましょう。ここまで説明のためにビューに色々なクラスを作成してきたので、少し整理します。まず、ビューですが、thread/views.pyにはTopicCreateViewクラスが以下のように書かれている筈です。
thread/views.py(一部抜粋)
class TopicCreateView(CreateView):
template_name = 'thread/create_topic.html'
form_class = TopicModelForm
model = Topic
success_url = reverse_lazy('base:top')
そしてthread/urls.pyはlocalhost:8080/create_topic/にアクセスされた場合にトピック作成画面を表示するように以下の様になっています。
thread/urls.py(一部抜粋)
urlpatterns = [
path('create_topic/', views.TopicCreateView.as_view(), name='create_topic'),
# path('create_topic/', views.topic_create, name='create_topic'),
path('<int:pk>/', views.TopicTemplateView.as_view(), name='topic'),
]
登録が成功した場合にはbaseアプリケーションのTOPが表示され、登録されたトピックが一番上に表示されるようになっていますね。
確認画面テンプレートの作成
では確認用の画面を作るわけですからテンプレートを追加しましょう。今回はtemplates/thread/confirm_topic.htmlと名付けましょう。
templates/thread/confirm_topic.html
{% extends 'base/base.html' %}
{% block title %}トピック作成 - {{ block.super }}{% endblock %}
{% block content %}
<div class="ui grid stackable">
<div class="eleven wide column">
<div class="ui breadcrumb">
<a href="{% url 'base:top' %}" class="section">TOP</a>
<i class="right angle icon divider"></i>
<a class="active section">トピック作成</a>
</div>
<div class="ui segment">
<div class="content">
<div class="header"><h3>トピック作成</h3></div>
<p>内容を確認してください</p>
<table class="ui celled table table table-hover" >
<tr><td>タイトル</td><td>{{form.title.value}}</td></tr>
<tr><td>お名前</td><td>{{form.user_name.value}}</td></tr>
<tr><td>カテゴリー</td><td>{{form.cleaned_data.category}}</td></tr>
<tr><td>本文</td><td><pre>{{form.message.value}}</pre></td></tr>
</table>
<form class="ui form" action="{% url 'thread:create_topic' %}" method="POST">
{% csrf_token %}
{% for field in form %}
{{field.as_hidden}}
{% endfor %}
<button class="ui button grey" type="submit" name="next" value="back">戻る</button>
<button class="ui button orange" type="submit" name="next" value="create">作成</button>
</form>
</div>
</div>
</div>
{% include 'base/sidebar.html' %}
</div>
{% endblock %}
注目点は3つあります。まず、カテゴリーのところだけ書き方が変わっていますね。form.category.valueにはカテゴリーIDの数値しか入っておらず、確認画面で表示してもユーザーには分かりません。そこでform_valid関数通過後に生成される検証済みのデータ(cleaned_data)からカテゴリーを取得して表示しています。
二つ目は{{field.as_hidden}}ですね。これは{{form.as_p}}と同様にHTMLを返す関数で、インプットタグのタイプをhiddenとしてくれます。確認画面を作る場合セッションにデータを保持する方法もありますが、今回はhiddenのインプットタグで再度POSTする方式を取ります。
3つ目ですが、戻るボタンと作成ボタンのname属性とvalue属性ですね。つまりPOSTする度にnextパラメータに次にどちらに進むかを司令を出すというわけです。
次に既に作成済みのtemplates/thread/create_topic.htmlにも手を加えましょう。
templates/thread/create_topic.html(一部抜粋)
- <button type="submit" class="ui button">作成</button>
+ <button type="submit" class="ui button" name="next" value="confirm">作成</button>
先程と同様にname属性とvalue属性を追加しました。つまり確認画面に進めという指示を送るということです。
ビューの作成
ではビューの作成に入っていきましょう。既に作成済みのTopicCreateViewを改良していきます。TopicCreateViewはDjagnoのクラスベースビューであるCreateViewを継承したクラスです。
thread/views.py(一部抜粋)
class TopicCreateView(CreateView):
template_name = 'thread/create_topic.html'
form_class = TopicModelForm
model = Topic
success_url = reverse_lazy('base:top')
def form_valid(self, form):
ctx = {'form': form}
if self.request.POST.get('next', '') == 'confirm':
return render(self.request, 'thread/confirm_topic.html', ctx)
if self.request.POST.get('next', '') == 'back':
return render(self.request, 'thread/create_topic.html', ctx)
if self.request.POST.get('next', '') == 'create':
return super().form_valid(form)
else:
# 正常動作ではここは通らない。エラーページへの遷移でも良い
return redirect(reverse_lazy('base:top'))
ソースコードを見たままなのですが、基本的に手を加えたのはform_valid関数のオーバーライドのみです。POSTされたnext値によって場合わけして表示するテンプレートを分けています。’confirm’の場合は先程作成したconfirm_topic.htmlを表示しています。’back’であった場合には受けたformをそのまま渡して入力画面であるtopic_create.htmlを表示しています。これにより、ユーザーの入力値は保持されたまま表示されます。’create’であった場合にはCreateViewのform_valid関数を呼び出して保存処理を行いsuccess_urlに遷移します。これらどれでもない場合は異常系動作となりますが、今回はトップページに飛ばしました。エラーページなどを作成してそちらに飛ばしても良いと思います。
確認してみましょう。
入力画面
確認画面
登録後
最後に
データ検証をフォームが担ってくれているので、基本的にはビューの仕事は見せるべきデータを揃えてテンプレート渡すだけ、という分かりやすい例かなと考えています。一章もそろそろ大詰めですね。次回はカテゴリ毎にトピック一覧表示するページをを作りましょう。
Sponsored Link
こんにちは。djangoの大変為になる情報をありがとうございます。
ところで、こちらの実装方法を試してみたところ、確認画面に画像ファイルだけ引き継がれませんでした。他の文字等のフィールドは全く問題ありません。
おそらくrequest.FILEだけ画面遷移時に引き継がれていないのだと思うのですが、上記の実装方法でこの問題を解決する方法はありますでしょうか。
ご教示いただけましたら幸いです。宜しくお願いいたします。
農家さん
コメントありがとうございます。確認ですが、ファイルをアップロードする画面のHTMLのformタグの属性がenctype=”multipart/form-data”となっていますでしょうか?こうしないと遷移先でファイルが取得できません。
画像ファイルはrequest.FILESからも取得できますが、Djangoのフォームまたはモデルフォームを使用した場合、form_valid関数内でcleaned_dataから取り出すことが可能です。こうしてバリデーション処理済みのファイル(TemporaryUploadedFileオブジェクトだったと記憶してます)を取り出すことで安全にファイルにアクセスできます。ファイルオブジェクトにアクセスできればその後の実装はファイルの保存処理とファイルパスの取得や保存なので特に問題ないと思います。
参考になれば幸いです。
クロさん
ご返信ありがとうございます。
現状の確認画面のhtmlは以下の通りです。
{% csrf_token %}
出品果物名(最大20字)必須
{{ form.product_name }}
{{ form.product_name.errors }}
商品画像
{{ form.product_image }}
{{ form.product_image.errors }}
確認画面へ進む
以下のview.pyのif文のコードの中で、
1) if self.request.POST.get(‘next’, ”) == ‘confirm’:
2) if self.request.POST.get(‘next’, ”) == ‘back’:
3) if self.request.POST.get(‘next’, ”) == ‘create’:
product_image = form.cleaned_data.get(‘product_image’)によってフォームからイメージファイルを取り出し、print(product_image)、print(type(product_image))としてみたところ、
1)では grapes-3.jpg
が表示され、2)および3)の戻るボタン、登録ボタンを押した状態では、上記のprint文の中身が None のように空になってしまいました。
以下でも同じような質問があったため、同じ症状ではないかと思っておりました。
https://ja.stackoverflow.com/questions/33067/%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%8C%E7%A2%BA%E8%AA%8D%E7%94%BB%E9%9D%A2%E3%81%B8%E3%81%AEpost%E5%BE%8C%E6%B6%88%E3%81%88%E3%81%A6%E3%81%97%E3%81%BE%E3%81%86
その場合、セッションやキャッシュ等に一時的にImageFieldのクラスオブジェクトだけは保存しなくてはならないようで、その実装方法に関してもなかなか手が付けられず途方に暮れていました。
長々とすみません。何か私の勘違いや見落とし、誤り等あったらご指摘いただけると幸いです。宜しくお願いいたします。
度々すみません。コメントでは、htmlのタグが全てエスケープされ消えてしまったようです。タグを全て全角に置き換えました。確認画面のhtmlは以下の通りです。
<form action=”{% url ‘app:product_register’ user.farm.pk %}” enctype=”multipart/form-data” accept-charset=”UTF-8″
method=”POST”>
{% csrf_token %}
<div class=”select-content”>
<h3>出品果物名(最大20字)<span>必須</span></h3>
{{ form.product_name }}
{{ form.product_name.errors }}
</div>
<div class=”select-content”>
<h3>商品画像</h3>
{{ form.product_image }}
{{ form.product_image.errors }}
</div>
<div>
<button type=”submit” class=”sell-submit” name=”next” value=”confirm”>確認画面へ進む</button>
</div>
</form>
また、
print(type(product_image))の中身は以下です。
<class ‘django.core.files.uploadedfile.InMemoryUploadedFile’>
農家さん
コメントありがとうございます。
貼っていただいたリンク先にも回答があるように、一度フォームから渡されたファイルを一時保管する必要が出てきます。実装方法の1つとして一時保管用のディレクトリを使用する場合、まずcleaned_dataから取得したファイルをdefault_storage等を利用し一時保管用のディレクトリに保存しファイルパスを取得します。そのファイルパスをセッションに保持するか、hiddenのフォームで完了画面に送り、完了画面表示処理部分でファイルパスをDBに保存するという流れで、やりたいことは実現できるかと思います。
個別の案件にこのブログでこれ以上回答するのは適当でないと思いますので、詳細な実装方法についてはメールででもご相談いただければと思います。連絡先は当ブログのサイドバーにありますので、よろしくお願いいたします。