A Complete Beginner's Guide to Djangoのチュートリアルを参考にフォームを作成してみる。
フォームを表示するページ作成
DjangoではフォームAPIを使ってフォームを作成するが、理解を深めるためにまずはフォームAPIを使わずにフォームを作成してみる。そして、その後でフォームAPIを使ってフォームを作成する。
今回作成するフォームは新しいTopic(Board内のスレッド)とPost(Topicに対する返信だが、Topic作成時のメッセージも含む)を作成するフォームで完成形は下図。実際は誰がTopicとPostを作成したかを管理するがユーザ認証等については後で考える。
最初にフォームを表示するページを作成するためにURLconfを修正してnew_topicの行を追加する。
from django.contrib import admin from django.conf import settings from django.urls import path, include from boards import views urlpatterns = [ path('', views.home, name='home'), path('boards/<int:pk>/', views.board_topics, name='board_topics'), path('boards/<int:pk>/new/', views.new_topic, name='new_topic'), path('admin/', admin.site.urls), ]
次にboards/views.pyにnew_topic()を追加する。
from django.shortcuts import render, get_object_or_404 from .models import Board def new_topic(request, pk): board = get_object_or_404(Board, pk=pk) return render(request, 'new_topic.html', {'board': board})
そしてtemplates/new_topic.htmlというテンプレートを作成する。
{% extends 'base.html' %}
{% block title %}Start a New Topic{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
<li class="breadcrumb-item"><a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a></li>
<li class="breadcrumb-item active">New topic</li>
{% endblock %}
{% block content %}
{% endblock %}
これでboards/1/new/にアクセスするとパンくずリストが表示されるようになる。
new_topic用のテストを追加しておく。前に作成したテストとほぼ同じだがnew_topicのimportを忘れないこと。
from django.urls import reverse, resolve from django.test import TestCase from .views import home, board_topics, new_topic from .models import Board class HomeTests(TestCase): # ... class BoardTopicsTests(TestCase): # ... class NewTopicTests(TestCase): def setUp(self): Board.objects.create(name='Django', description='Django board.') def test_new_topic_view_success_status_code(self): url = reverse('new_topic', kwargs={'pk': 1}) response = self.client.get(url) self.assertEquals(response.status_code, 200) def test_new_topic_view_not_found_status_code(self): url = reverse('new_topic', kwargs={'pk': 99}) response = self.client.get(url) self.assertEquals(response.status_code, 404) def test_new_topic_url_resolves_new_topic_view(self): view = resolve('/boards/1/new/') self.assertEquals(view.func, new_topic) def test_new_topic_view_contains_link_back_to_board_topics_view(self): new_topic_url = reverse('new_topic', kwargs={'pk': 1}) board_topics_url = reverse('board_topics', kwargs={'pk': 1}) response = self.client.get(new_topic_url) self.assertContains(response, 'href="{0}"'.format(board_topics_url))
テストが通ることを確認。
$ python manage.py test Creating test database for alias 'default'... System check identified no issues (0 silenced). ........... ---------------------------------------------------------------------- Ran 11 tests in 0.051s OK Destroying test database for alias 'default'...
フォーム作成
ページができたのでフォームを作成する。
templates/new_topic.htmlの{% block content %}内を以下のように編集する。
{% extends 'base.html' %}
{% block title %}Start a New Topic{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
<li class="breadcrumb-item"><a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a></li>
<li class="breadcrumb-item active">New topic</li>
{% endblock %}
{% block content %}
<form method="post">
{% csrf_token %}
<div class="form-group">
<label for="id_subject">Subject</label>
<input type="text" class="form-control" id="id_subject" name="subject">
</div>
<div class="form-group">
<label for="id_message">Message</label>
<textarea class="form-control" id="id_message" name="message" rows="5"></textarea>
</div>
<button type="submit" class="btn btn-success">Post</button>
</form>
{% endblock %}
これでBootstrap 4のCSSクラスが適用されたフォームが表示される。
新しいTopicを作成するので<form method="post">とPOSTメソッドを使用するが、DjangoではPOSTメソッドを使用する場合はCSRF(Cross-site Request Forgery)対策としてCSRFトークンを渡す必要がある。
{% csrf_token %}がそのタグで以下のようなHTMLに変換される。
<input type='hidden' name='csrfmiddlewaretoken' value='a9tL0UDbdkCA5R372UD4zbUHH3qxx6hq2zcov43t5rhUDWQY9BRduzLfIHUmq5KB' />
その他、inputタグのname属性で指定した値は、
<input type="text" class="form-control" id="id_subject" name="subject"> <textarea class="form-control" id="id_message" name="message" rows="5"></textarea>
ビュー側で以下のように指定することで参照できる。
subject = request.POST['subject'] message = request.POST['message']
ビューの処理
フォームで入力されたデータを受け取って新しいTopicとPostを作成するビューの処理をboards/views.pyに記述する。
from django.contrib.auth.models import User from django.shortcuts import render, redirect, get_object_or_404 from .models import Board, Topic, Post def new_topic(request, pk): board = get_object_or_404(Board, pk=pk) if request.method == 'POST': subject = request.POST['subject'] message = request.POST['message'] user = User.objects.first() # TODO: get the currently logged in user topic = Topic.objects.create( subject=subject, board=board, starter=user ) post = Post.objects.create( message=message, topic=topic, created_by=user ) return redirect('board_topics', pk=board.pk) # TODO: redirect to the created topic page return render(request, 'new_topic.html', {'board': board})
フォームAPIを使えば未入力チェックや、max_lengthを超える場合のバリデーションができるが、上記の処理は入力が適切な場合しか考慮されていないので注意。
if request.method == 'POST':の分岐はPOSTメソッドの場合、つまりフォームで入力されて送信された場合はこの処理を通る。一方、GETメソッドだった場合、つまりブラウザで普通にアクセスした場合はこの処理は通らずにrender()が呼ばれるのでフォームが表示されることになる。
ユーザ認証についてはまだ検討していないのでUser.objects.first()で一番最初のユーザ(この場合はadminユーザ)を取得している。
Topic.objects.createのboard=boardやPost.objects.createのtopic=topicのようにモデルでForeignKey()を指定した箇所はこのようにして関連付ける。
また、TopicとPost一覧を表示するページはまだ作成していないので処理が完了したらboard_topicsページにリダイレクトするようにする。
ビューの処理を記述したのでSubjectとMessageに値を入力した後でPostボタンをクリックする。
ページがリダイレクトされ登録は完了しているようだが、Topic一覧を表示する処理を書いていないので何も表示されない。
Topic一覧表示
Topic一覧を表示するようにtemplates/topics.htmlを編集する。
{% extends 'base.html' %}
{% block title %}
{{ board.name }} - {{ block.super }}
{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
<li class="breadcrumb-item active">{{ board.name }}</li>
{% endblock %}
{% block content %}
<table class="table">
<thead class="thead-dark">
<tr>
<th>Topic</th>
<th>Starter</th>
<th>Replies</th>
<th>Views</th>
<th>Last Update</th>
</tr>
</thead>
<tbody>
{% for topic in board.topics.all %}
<tr>
<td>{{ topic.subject }}</td>
<td>{{ topic.starter.username }}</td>
<td>0</td>
<td>0</td>
<td>{{ topic.last_updated }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
これで先ほど登録したTopicが表示されるようになる。
BoardとTopicは1対多の関係のため、Boardには複数のTopicが関連付けられている。それらは{% for topic in board.topics.all %}のようにして取り出すことができる(テンプレート言語ではall()ではなくallとなる)。
また、関連付けられているモデルの属性には{{ topic.starter.username }}のように.で繋げてアクセスできる。
Topic作成ボタン追加
フォームは作成できたのでtemplates/topics.htmlを編集してTopicを作成するボタンを追加する。
{% block content %}
<div class="mb-4">
<a href="{% url 'new_topic' board.pk %}" class="btn btn-primary">New topic</a>
</div>
<table class="table">
<!-- コードは省略 -->
</table>
{% endblock %}
そして、このボタンリンクをテストするために既に作成しているtest_board_topics_view_contains_link_back_to_homepage()を下記テスト名にリネームしてassertContains()を追加しておく。
class BoardTopicsTests(TestCase): # ... def test_board_topics_view_contains_navigation_links(self): board_topics_url = reverse('board_topics', kwargs={'pk': 1}) homepage_url = reverse('home') new_topic_url = reverse('new_topic', kwargs={'pk': 1}) response = self.client.get(board_topics_url) self.assertContains(response, 'href="{0}"'.format(homepage_url)) self.assertContains(response, 'href="{0}"'.format(new_topic_url))
以上のようにフォームAPIを使わずにフォームを作成することはできたがバリデーションなどができていないため、次回はフォームAPIを使ってフォームを作成する。
まとめ
- POSTメソッドではCSRF対策としてCSRFトークンを渡す必要
{% csrf_token %}でCSRFトークンを生成- ビュー側ではinputタグのname属性の値を
request.POST['subject']のように指定 - テンプレート言語では
{% for topic in board.topics.all %}のようにループ - 関連付けられているモデルの属性には
{{ topic.starter.username }}のようにアクセス