A Complete Beginner's Guide to Djangoのチュートリアルを参考にフォームAPIを使用してフォームを作成してみる。
フォームのテストを追加
前回フォームAPIを使用せずにフォームを作成したので、まずはそれに対するテストを追加する。
boards/tests.py
のNewTopicTests
を以下のように編集する。
from django.urls import reverse, resolve from django.test import TestCase from django.contrib.auth.models import User from .views import home, board_topics, new_topic from .models import Board, Topic, Post class NewTopicTests(TestCase): def setUp(self): Board.objects.create(name='Django', description='Django board.') User.objects.create_user(username='john', email='john@doe.com', password='123') # <- included this line here # ... def test_csrf(self): url = reverse('new_topic', kwargs={'pk': 1}) response = self.client.get(url) self.assertContains(response, 'csrfmiddlewaretoken') def test_new_topic_valid_post_data(self): url = reverse('new_topic', kwargs={'pk': 1}) data = { 'subject': 'Test title', 'message': 'Lorem ipsum dolor sit amet' } response = self.client.post(url, data) self.assertTrue(Topic.objects.exists()) self.assertTrue(Post.objects.exists()) def test_new_topic_invalid_post_data(self): ''' Invalid post data should not redirect The expected behavior is to show the form again with validation errors ''' url = reverse('new_topic', kwargs={'pk': 1}) response = self.client.post(url, {}) self.assertEquals(response.status_code, 200) def test_new_topic_invalid_post_data_empty_fields(self): ''' Invalid post data should not redirect The expected behavior is to show the form again with validation errors ''' url = reverse('new_topic', kwargs={'pk': 1}) data = { 'subject': '', 'message': '' } response = self.client.post(url, data) self.assertEquals(response.status_code, 200) self.assertFalse(Topic.objects.exists()) self.assertFalse(Post.objects.exists())
それぞれのテストの意味は以下の通り。
setUp
:User.objects.create_user
でテストで使用するUser
インスタンスを作成するtest_csrf
: CSRF対策用のCSRFトークンが含まれているか確認するtest_new_topic_valid_post_data
: 適切なデータがPOSTされた場合にTopic
,Post
インスタンスが存在することを確認するtest_new_topic_invalid_post_data
: データなしでPOSTされた場合にステータスコードが200であることを確認する(バリデーションエラーと共にフォームが再表示されることを期待しているので)test_new_topic_invalid_post_data_empty_fields
: 空文字のデータがPOSTされた場合にステータスコードが200であることとTopic
,Post
インスタンスが存在しないことを確認する
現在の実装に対してテストを実行すると1 ERROR、1 FAILEDになる。
両方とも入力データのバリデーションに関することで、フォームAPIを使用せずに作成したフォームではバリデーションが考慮されていない。
以下、これらのテストが通るようにフォームAPIを使用してフォームを作成するように変更する。
$ python manage.py test Creating test database for alias 'default'... System check identified no issues (0 silenced). .........EF.... ====================================================================== ERROR: test_new_topic_invalid_post_data (boards.tests.NewTopicTests) ---------------------------------------------------------------------- Traceback (most recent call last): ... django.utils.datastructures.MultiValueDictKeyError: 'subject' ====================================================================== FAIL: test_new_topic_invalid_post_data_empty_fields (boards.tests.NewTopicTests) ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/vagrant/django/myproject/myproject/boards/tests.py", line 112, in test_new_topic_invalid_post_data_empty_fields self.assertEquals(response.status_code, 200) AssertionError: 302 != 200 ---------------------------------------------------------------------- Ran 15 tests in 1.243s FAILED (failures=1, errors=1) Destroying test database for alias 'default'...
フォームAPI : ビュー側
フォームAPIはdjango.forms
モジュールで利用できforms.Form
とforms.ModelForm
の2種類のフォームがある。
forms.Form
は直接モデルと対応していないフォームの作成にも使える汎用的なクラスで、forms.ModelForm
はモデルに対応した適切なフィールドのフォームを簡単に作成できるクラス。
今回はTopicモデルに対応したフォームを作成するのでforms.ModelForm
を使用して下記内容のboards/forms.py
を作成する。
from django import forms from .models import Topic class NewTopicForm(forms.ModelForm): message = forms.CharField(widget=forms.Textarea(), max_length=4000) class Meta: model = Topic fields = ['subject', 'message']
class Meta
内のmodel = Topic
でTopicモデルと対応していること示しており、fields
内のsubject
はTopicモデルのsubjectと対応する。
message
はTopicモデルではなくPostモデルに含まれるフィールドなのでmessage = forms.CharField()
のように明示的に指定する必要がある。
次にboards/views.py
を以下のように修正する。
from django.contrib.auth.models import User from django.shortcuts import render, redirect, get_object_or_404 from .forms import NewTopicForm from .models import Board, Topic, Post def new_topic(request, pk): board = get_object_or_404(Board, pk=pk) user = User.objects.first() # TODO: get the currently logged in user if request.method == 'POST': form = NewTopicForm(request.POST) if form.is_valid(): topic = form.save(commit=False) topic.board = board topic.starter = user topic.save() post = Post.objects.create( message=form.cleaned_data.get('message'), topic=topic, created_by=user ) return redirect('board_topics', pk=board.pk) # TODO: redirect to the created topic page else: form = NewTopicForm() return render(request, 'new_topic.html', {'board': board, 'form': form})
if request.method == 'POST':
の分岐はPOSTメソッドの場合、つまりフォームで入力されて送信された場合はこの処理を通りform = NewTopicForm(request.POST)
により送信されたデータでインスタンスが生成される。
一方、GETメソッドだった場合、つまりブラウザで普通にアクセスした場合はNewTopicForm()
が初期化されrender()
により空のフォームが表示されることになる。
POSTメソッドの場合は次にif form.is_valid():
によりデータのバリデーションを実行する。バリデーションに成功した後はtopic
とpost
に送信されたデータをセットしてデータベースに保存し、別ページへリダイレクトする。
バリデーションに失敗した場合は、フォームインスタンスにバリデーションエラーが追加された後でrender()
が呼ばれるので、バリデーションエラーメッセージありのフォームが表示される。
先ほどのテストを実行してみると全部のテストが通ることが確認できる。
$ python manage.py test Creating test database for alias 'default'... System check identified no issues (0 silenced). ............... ---------------------------------------------------------------------- Ran 15 tests in 1.177s OK Destroying test database for alias 'default'...
フォームAPI : テンプレート側
テンプレート側でもフォームAPIを使用するように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 %} <form method="post"> {% csrf_token %} {{ form.as_p }} <button type="submit" class="btn btn-success">Post</button> </form> {% endblock %}
複数行書いていた処理が{{ form.as_p }}
の1行だけで置き換えられる。
ページにアクセスするとフォームが表示されるが、Bootstrapを適用した書き方ではないので見た目は悪くなる(後で修正する)。
HTMLの出力方法にはas_p
の他にas_ul
, as_table
もありそれぞれの出力は以下の通り。
as_p
の場合(クリックで展開)
<p> <label for="id_subject">Subject:</label> <input type="text" name="subject" maxlength="255" required id="id_subject" /> </p> <p> <label for="id_message">Message:</label> <textarea name="message" cols="40" rows="10" maxlength="4000" required id="id_message"> </textarea> </p>
as_ul
の場合(クリックで展開)
<li> <label for="id_subject">Subject:</label> <input type="text" name="subject" maxlength="255" required id="id_subject" /> </li> <li> <label for="id_message">Message:</label> <textarea name="message" cols="40" rows="10" maxlength="4000" required id="id_message"> </textarea> </li>
as_table
の場合(クリックで展開)
<tr> <th> <label for="id_subject">Subject:</label> </th> <td> <input type="text" name="subject" maxlength="255" required id="id_subject" /> </td> </tr> <tr> <th> <label for="id_message">Message:</label> </th> <td> <textarea name="message" cols="40" rows="10" maxlength="4000" required id="id_message"> </textarea> </td> </tr>
見た目は悪くてもフォームとしては動作するので、バリデーションが動作するか何も入力せずにPostボタンをクリックしてみる。
上記のメッセージが表示されたが、これはDjangoではなくブラウザ(上の図はChrome)が出しているメッセージのため、Djangoの動作が確認できるようにnovalidate
属性を追加して無効にする。
<form method="post" novalidate>
再度PostボタンをクリックするとDjangoが出しているバリデーションエラーメッセージが確認できる。
なお、Form
クラスでは以下のようにヘルプテキスト、その他の属性(プレースホルダなど)を指定することができる。
from django import forms from .models import Topic class NewTopicForm(forms.ModelForm): message = forms.CharField( widget=forms.Textarea( attrs={'rows': 5, 'placeholder': 'What is on your mind?'} ), max_length=4000, help_text='The max length of the text is 4000.' ) class Meta: model = Topic fields = ['subject', 'message']
まとめ
forms.Form
とforms.ModelForm
の2種類のフォームがあるforms.ModelForm
はモデルに対応したフォームを作成するis_valid()
でバリデーションの結果を確認できる{{ form.as_p }}
でフォームのHTMLを生成できるas_p
の他にas_ul
,as_table
もある