Djangoメモ(16) : フォームAPIを使わずにフォームを作成
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 }}
のようにアクセス