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もある