Djangoフォーム
私たちのWebサイトで最終的にやりたいことは、記事を追加したり編集したりするためのよい方法を作ることです。 Django admin
はかなりいいですが、カスタマイズしたりかわいくいい感じにするのはちょっと大変です。 フォーム
によってインターフェイスを完璧にコントロールできるようになります。想像するほとんど全てのことができます!
Djangoフォームのよいところは、フォームをゼロから定義できたり、フォームの結果をモデルに保存できるModelForm
を作れたりするところです。
これはまさに私たちがやりたいことです:Post
モデルのためのフォームを作ります。
Djangoの他の重要なパーツと同様に、フォームは自身のファイルがあります: forms.py
これはblog
ディレクトリの下にforms.pyの名前で作る必要があります。
blog
└── forms.py
このファイルをエディタで開き、次のコードを入力してください。
blog/forms.py
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ('title', 'text',)
最初にDjangoのformsをインポート(from django import forms
)し、Post
モデルもインポート(from .models import Post
)する必要があります。
PostForm
とは何かと思うかもしれませんが、これはフォームの名前です。 このフォームが ModelForm
の一種だとDjangoに伝える必要があります (Djangoが私たちのためにいくつか魔法をかけられるように)。forms.ModelForm
がその役割を果たします。
次にclass Meta
ですが、ここでDjangoにフォームを作るときにどのモデルを使えばいいか (model = Post
) を伝えます。
最後にフォームのフィールドに何を置くか書きます。 ここでは、私たちはtitle
(タイトル)と text
(本文)のみをフォームで使用します。 author
は現在ログインしている人(あなた)です。 created_date
は(コードによって)自動的に記事を書いた日時が設定されます。
ひとまずこれでおしまいです!あとはフォームをビューで使い、それをテンプレート内に表示しさえすればいいです。
もう一度、ページへのリンク、URL、ビューとテンプレートを作ります。
フォームにおけるページへのリンク
blog/templates/blog/base.html
をエディタで開きましょう。page-header
と名付けた div
中に次のリンクを追加します:
blog/templates/blog/base.html
<a href="{% url 'post_new' %}" class="top-menu"><span class="glyphicon glyphicon-plus"></span></a>
新しいビューpost_new
を呼び出すことに注意してください。 "glyphicon glyphicon-plus"
クラスは、使用しているBootstrapテーマによって提供され、プラス記号を表示します。
行を追加すると、このような html ファイルになります。
blog/templates/blog/base.html
{% load static %}
<html>
<head>
<title>Django Girls blog</title>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css">
<link href='//fonts.googleapis.com/css?family=Lobster&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="{% static 'css/blog.css' %}">
</head>
<body>
<div class="page-header">
<a href="{% url 'post_new' %}" class="top-menu"><span class="glyphicon glyphicon-plus"></span></a>
<h1><a href="/">Django Girls Blog</a></h1>
</div>
<div class="content container">
<div class="row">
<div class="col-md-8">
{% block content %}
{% endblock %}
</div>
</div>
</div>
</body>
</html>
ファイルを保存して、ページ http://127.0.0.1:8000 をリロードすると見覚えのある NoReverseMatch
エラーが表示されると思います。実際にそうなってますか?いいですね!
URL
blog/urls.py
をエディタで開き、次の行を追加します。
blog/urls.py
path('post/new/', views.post_new, name='post_new'),
すると最終的なコードは次のようになります:
blog/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.post_list, name='post_list'),
path('post/<int:pk>/', views.post_detail, name='post_detail'),
path('post/new/', views.post_new, name='post_new'),
]
サイトをリロードした後、AttributeError
が出ます。post_new
ビューの実装がないからです。ファイルに追加してみましょう。
post_new ビュー
blog/views.py
をエディタで開き、from
の行の後に次の内容を追加してみましょう。
blog/views.py
from .forms import PostForm
その後にビューを追加します。
blog/views.py
def post_new(request):
form = PostForm()
return render(request, 'blog/post_edit.html', {'form': form})
Post
フォームを新しく作るには、PostForm()
を呼び出し、それをテンプレートに渡す必要があります。 あとでこの ビュー に戻ってきますが、今はフォームのためのテンプレートをすぐに作ってしまいましょう。
テンプレート
blog/templates/blog
ディレクトリにpost_edit.html
ファイルを作り、エディタで開きましょう。フォームを動かすにはいくつかやることがあります。
- フォームを表示する必要があります。 私たちは(例えば)
{{ form.as_p }}
でこれを行うことができます。 - 上記の行は HTMLのformタグでラップする必要があります:
<form method="POST">...</form>
Save
ボタンが必要です。これをHTMLのbuttonタグで行います:<button type="submit">Save</button>
- 最後に
<form ...>
タグの開始直後に、{% csrf_token %}
を追加する必要があります。 フォームをセキュアにするためこれは非常に重要です! これを忘れると、Djangoはフォームを保存しようとすると文句を言うでしょう:
では、post_edit.html
のHTMLがどのようになるか見てみましょう:
blog/templates/blog/post_edit.html
{% extends 'blog/base.html' %}
{% block content %}
<h2>New post</h2>
<form method="POST" class="post-form">{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="save btn btn-default">Save</button>
</form>
{% endblock %}
更新をしてみましょう。やった!フォームが表示されます。
ちょっと待ってみて下さい。title
と text
フィールドに何か入力して保存するとどうなりますか?
何も起きません!もう一度同じページに戻りテキストはどこかに行ってしまいました… そして新しい投稿は追加されていません。何がいけなかったのでしょうか?
答えは: 何も間違ってない、です。ビュー でもう少し作業を行う必要があります.
フォームを保存する
blog/views.py
をもう一度エディタで開きます。現在の post_new
ビューはこうなっています。
blog/views.py
def post_new(request):
form = PostForm()
return render(request, 'blog/post_edit.html', {'form': form})
フォームを送信したとき、同じビューに戻されていましたが、このときrequest
、もっと詳しくいうと request.POST
にデータが追加されています (このPOSTという名前はブログ投稿 "post" とは関係ありません。このデータは送られてきたもの、というコトと関係しています) 。 HTMLファイルの <form>
タグで、method="POST"
という変数があったのを覚えていますか? これによってフォームのすべてのフィールドは今 request.POST
にあります。 POST
という名前を何か別のものに変えることはできません (他に唯一の有効な method
の値は GET
ですが、その違いを説明する時間がありません) 。
私たちの ビュー では、扱わなくてはならない2つの別々のシチュエーションがあります: 1つ目は、最初にページにアクセスしてきた時で空白のフォームが必要な場合。2つ目はすべてのフォームデータが入力された状態でビューに戻ってくる場合です。 したがって条件分岐を追加する必要があります(そのためにif
を使います):
blog/views.py
if request.method == "POST":
[...]
else:
form = PostForm()
ドット [...]
の部分を埋めていきましょう。 method
がPOST
の場合、フォームのデータを使ってPostForm
を構築します。 私たちはそれを次のようにします:
blog/views.py
form = PostForm(request.POST)
次にフォームの値が正しいかどうかをチェックします(すべての必須フィールドが設定され、不正な値が送信されないこと)。 form.is_valid()
で行います。
フォームをチェックして、フォームの値が有効であれば保存できます。
blog/views.py
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.published_date = timezone.now()
post.save()
基本的にここでは2つのことを行います。まず form.save
でフォームを保存することと author を追加することです (PostForm
内に author
フィールドがありませんし、このフィールドは必須です) 。 commit=False
は Post
モデルをまだ保存しないという意味です。保存前に author を追加したいので。 ほとんどの場合、commit=False
なしでform.save()
を使用しますが、この場合はそれを指定する必要があります。 post.save()
は変更を保存し(作成者を追加しつつ)、新しいブログ投稿が作成されます!
最後に、新しく作成された記事の post_detail
ページを表示できれば良いですよね? そのために次のインポートを追加します:
blog/views.py
from django.shortcuts import redirect
ファイルの先頭に追加します。これで新しく作成されたポストの post_detail
ページに移動する処理を書けます。
blog/views.py
return redirect('post_detail', pk=post.pk)
post_detail
は移動したいビューの名前です。 この ビュー では pk
変数が必須であることを覚えていますか? ビューにそれを渡すため、pk=post.pk
を使います。この post
は新しく作られたブログポストです!
ふー、たくさんのことを話してきましたが、そろそろ ビュー の全体がどんな感じか見てみたい頃じゃないでしょうか?
blog/views.py
def post_new(request):
if request.method == "POST":
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.published_date = timezone.now()
post.save()
return redirect('post_detail', pk=post.pk)
else:
form = PostForm()
return render(request, 'blog/post_edit.html', {'form': form})
では動作確認してみましょう。 http://127.0.0.1:8000/post/new/ に行き、 title
と text
を追加し、保存すると…… じゃじゃーん! 新しいブログ記事が追加され、post_detail にリダイレクトされます!
ブログ記事を保存する前に公開日をセットしていることに気づいたかもしれません。後ほど、Django Girls Tutorial: Extensionsにて 公開ボタン を導入します。
素晴らしい!
最近までDjango adminを使ってきたので、システム上で今まだログイン状態かと思います。 いくつかの状況ではログアウト状態になることがあります(ブラウザを閉じる、DBを再起動するなど..)。 投稿を作成するときに、ログインユーザーがわからないというエラーが発生した場合は、管理ページhttp://127.0.0.1:8000/admin にアクセスして再度ログインしてください。 その問題は一時的に解決します。 メインチュートリアルの後 Homework: add security to your website! の章に恒久的な対策がありますので宿題として取り組んでみてください。
フォームのバリデーション(検証)
ここではDjangoのフォームのクールなところを紹介します。 ブログのポストは title
と text
のフィールドが必要です。 Post
モデルではこれらのフィールドがなくてもよいとは書いておらず (published_date
とは対照的に)、Djangoはその場合、それらのフィールドには何らかの値が設定されることを期待します。
title
と text
を入力せずに保存してみましょう。何が起こるでしょうか?
Djangoはフォームのすべてのフィールドが正しいことを検証してくれます。気が利くでしょう?
フォームの編集
今、私たちは新しいフォームを追加する方法を知っています。 しかし既存のデータを編集するためはどうすれば良いのでしょうか? それは先ほど行ったことと非常に似ています。 すぐにいくつかの重要なものを作成してみましょう。 (もしわからない場合、コーチに尋ねるか、もしくはすでに手順をカバーしているので、前の章を見てください)
blog/templates/blog/post_detail.html
をエディタで開いて次の行を追加します
blog/templates/blog/post_detail.html
<a class="btn btn-default" href="{% url 'post_edit' pk=post.pk %}"><span class="glyphicon glyphicon-pencil"></span></a>
テンプレートは次のようになります:
blog/templates/blog/post_detail.html
{% extends 'blog/base.html' %}
{% block content %}
<div class="post">
{% if post.published_date %}
<div class="date">
{{ post.published_date }}
</div>
{% endif %}
<a class="btn btn-default" href="{% url 'post_edit' pk=post.pk %}"><span class="glyphicon glyphicon-pencil"></span></a>
<h2>{{ post.title }}</h2>
<p>{{ post.text|linebreaksbr }}</p>
</div>
{% endblock %}
blog/urls.py
をエディタで開き、次の行を追加します。
blog/urls.py
path('post/<int:pk>/edit/', views.post_edit, name='post_edit'),
テンプレート blog/templates/blog/post_edit.html
を再利用します。そして残るはビューです。
blog/views.py
をエディタで開いて次の内容をファイルの最後に追加します:
blog/views.py
def post_edit(request, pk):
post = get_object_or_404(Post, pk=pk)
if request.method == "POST":
form = PostForm(request.POST, instance=post)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.published_date = timezone.now()
post.save()
return redirect('post_detail', pk=post.pk)
else:
form = PostForm(instance=post)
return render(request, 'blog/post_edit.html', {'form': form})
post_new
とほとんど同じに見えますか? しかし完全に同じではありません。 まず urls
から追加の pk
パラメータを渡します。 次に編集したいPost
モデルを get_object_or_404(Post, pk=pk)
で取得し、フォームを作るときは以下の2つのケースのようにそのポストをinstance(インスタンス)
として渡します。フォームを保存するときは…
blog/views.py
form = PostForm(request.POST, instance=post)
…このポストを編集するためにただフォームを開く場合は:
blog/views.py
form = PostForm(instance=post)
よし、ちゃんと動くか試してみましょう!post_detail
ページにいきましょう。そこの右上に [編集] ボタンがあるはずです:
クリックするとブログ記事のフォームが表示されると思います:
あとはお気軽にタイトルやテキストを変更して保存してください!
おめでとう!アプリケーションが完成しました。
Djangoのフォームについてもっと知りたい場合、Djangoのドキュメントを読んでください。https://docs.djangoproject.com/ja/3.2/topics/forms/
セキュリティ
リンクをクリックするだけで新しい投稿を作成できることは素晴らしいことです! しかし、今、あなたのサイトにアクセスした人は誰でも新しいブログ投稿を作成することができます。それはおそらくあなたが望むものではありません。 ボタンはあなたのためには表示されますが、他の人には表示されないようにしましょう。
blog/templates/blog/base.html
をエディタで開き、page-header
と名付けた div
とそこに以前に入力したアンカータグを見つけます。 これは次のようになっています。
blog/templates/blog/base.html
<a href="{% url 'post_new' %}" class="top-menu"><span class="glyphicon glyphicon-plus"></span></a>
これに{% if %}
タグを追加し、管理者でログインしているユーザーのみにリンクを表示します。 今は、あなただけです! <a>
タグを以下のように変更します:
blog/templates/blog/base.html
{% if user.is_authenticated %}
<a href="{% url 'post_new' %}" class="top-menu"><span class="glyphicon glyphicon-plus"></span></a>
{% endif %}
この{% if %}
は、ページをリクエストしているユーザーがログインしている場合にのみ、リンクがブラウザに送信されるようにします。 これは新しい投稿の作成を完全に保護するものではありませんが、それは良い第一歩です。 私たちは拡張レッスンでより多くのセキュリティをカバーします。
詳細ページに追加した編集アイコンを覚えていますか? 他の人が既存の投稿を編集できないように、同じ変更を追加したいと思います。
blog/templates/blog/post_detail.html
をエディタで開いて次の行を見つけてください:
blog/templates/blog/post_detail.html
<a class="btn btn-default" href="{% url 'post_edit' pk=post.pk %}"><span class="glyphicon glyphicon-pencil"></span></a>
以下のように変更してください:
blog/templates/blog/post_detail.html
{% if user.is_authenticated %}
<a class="btn btn-default" href="{% url 'post_edit' pk=post.pk %}"><span class="glyphicon glyphicon-pencil"></span></a>
{% endif %}
あなたはログインしている可能性が高いので、ページを更新しても、何も変わらないかもしれません。 ただし、別のブラウザやシークレットウィンドウ(Windows Edgeでは「InPrivate」と呼ばれます)でページを読み込むと、リンクが表示されず、アイコンも表示されないでしょう!
もう一つ: デプロイの時間です!
ではPythonAnywhere上で動作するかを確認しましょう。再度デプロイします。
- まず、Githubにあなたの新しく書いたコードをCommitして、Pushしてみましょう。
command-line
$ git status
$ git add --all .
$ git status
$ git commit -m "Added views to create/edit blog post inside the site."
$ git push
- それから、PythonAnywhereのbashコンソールで:
PythonAnywhere command-line
$ cd ~/<your-pythonanywhere-domain>.pythonanywhere.com
$ git pull
[...]
(<your-pythonanywhere-domain>
の部分を、自分の実際のPythonAnywhereのサブドメイン名に山カッコをはずして置き換えることを忘れずに)
- 最後に、Webページ に飛んで(コンソールの右上のメニューボタンを使ってもいいですね)それから Reload を押しましょう。 変更を見るためにあなたのブログ https://subdomain.pythonanywhere.com を再読み込みしましょう。
うまくいってるはずです!おめでとう :)