Djangoメモ(18) : django-widget-tweaksを使用してBootstrapのフォームを作成する
A Complete Beginner's Guide to Djangoのチュートリアルを参考にdjango-widget-tweaks
を使用してBootstrapのフォームを作成してみる。
django-widget-tweaksのインストール
チュートリアルではdjango-widget-tweaks
を使用しているが、Django 2.0ではエラーが発生(後述)したのでdjango-widget-tweaks
をフォークしたdjango-widgets-improved
をインストールする(ただしこの記事ではdjango-widget-tweaks
として説明する)。
django-widget-tweaks
はテンプレートでフォームをレンダリングするときにCSSクラスやHTML属性を変更できるモジュール。
$ pip install django-widgets-improved
今回はPipenvで環境を構築しているので下記コマンドでインストール。
$ pipenv install django-widgets-improved
settings.py
のINSTALLED_APPS
に'widget_tweaks'
を追加する。
INSTALLED_APPS = [
...
'widget_tweaks',
...
]
テンプレートの編集
インストールが完了したらtemplates/new_topic.html
を編集する。
Bootstrapのフォームの使い方はドキュメントに書いてあるが、簡単にまとめると各フィールドを<div class="form-group">
で囲み、入力タグにclass="form-control"
を追加すればよい。
{% extends 'base.html' %} {% load widget_tweaks %} {% 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" novalidate> {% csrf_token %} {% for field in form %} <div class="form-group"> {{ field.label_tag }} {% render_field field class="form-control" %} {% if field.help_text %} <small class="form-text text-muted"> {{ field.help_text }} </small> {% endif %} </div> {% endfor %} <button type="submit" class="btn btn-success">Post</button> </form> {% endblock %}
最初に{% load widget_tweaks %}
タグによりdjango-widget-tweaks
をロードする。
{% render_field field class="form-control" %}
はdjango-widget-tweaks
のタグで最初の引数でフォームフィールドのインスタンスを指定し、次の引数で追加したい属性を指定する。
render_field
タグの使い方の例は以下の通り。
{% render_field form.subject class="form-control" %} {% render_field form.message class="form-control" placeholder=form.message.label %} {% render_field field class="form-control" placeholder="Write a message!" %} {% render_field field style="font-size: 20px" %}
これでBootstrapのCSSクラスが適用されたフォームが作成される。
HTML出力(クリックで展開)
<form method="post" novalidate>
<input type='hidden' name='csrfmiddlewaretoken' value='elpoqJPS00rjFpbgNQV3441yfVFd82aX6L81VTfaS76DduY7Ux9cZsS6gz9211D8' />
<div class="form-group">
<label for="id_subject">Subject:</label>
<input type="text" name="subject" maxlength="255" class="form-control" required id="id_subject" />
</div>
<div class="form-group">
<label for="id_message">Message:</label>
<textarea name="message" cols="40" rows="5" placeholder="What is on your mind?" maxlength="4000" class="form-control" required id="id_message">
</textarea>
<small class="form-text text-muted">
The max length of the text is 4000.
</small>
</div>
<button type="submit" class="btn btn-success">Post</button>
</form>
ただし、上記のコードだとバリデーションメッセージが表示されないので、バリデーションのコードを追加する。
<form method="post" novalidate> {% csrf_token %} {% for field in form %} <div class="form-group"> {{ field.label_tag }} {% if form.is_bound %} {% if field.errors %} {% render_field field class="form-control is-invalid" %} {% for error in field.errors %} <div class="invalid-feedback"> {{ error }} </div> {% endfor %} {% else %} {% render_field field class="form-control is-valid" %} {% endif %} {% else %} {% render_field field class="form-control" %} {% endif %} {% if field.help_text %} <small class="form-text text-muted"> {{ field.help_text }} </small> {% endif %} </div> {% endfor %} <button type="submit" class="btn btn-success">Post</button> </form>
レンダリング時には3つの状態があり、初期状態ではform.is_bound
がFalse
になるので通常のフォームが作成される。
バリデーション失敗状態では.is-invalid
クラスを追加し、.invalid-feedback
クラスでエラーメッセージを赤色で表示する。
バリデーション成功状態では.is-valid
クラスを追加し、入力データが適切だった場合にフィールドを緑色で表示する。
バリデーションメッセージ表示時のHTML出力(クリックで展開)
<form method="post" novalidate>
<input type='hidden' name='csrfmiddlewaretoken' value='NaS7H4NUyekGiA0zOghls6gq22ZWBq8FFABKcedcqlZ0QFNqVXvunu7Y3GtLupBQ' />
<div class="form-group">
<label for="id_subject">Subject:</label>
<input type="text" name="subject" maxlength="255" class="form-control is-invalid" required id="id_subject" />
<div class="invalid-feedback">
このフィールドは必須です。
</div>
</div>
<div class="form-group">
<label for="id_message">Message:</label>
<textarea name="message" cols="40" rows="5" placeholder="What is on your mind?" maxlength="4000" class="form-control is-invalid" required id="id_message">
</textarea>
<div class="invalid-feedback">
このフィールドは必須です。
</div>
<small class="form-text text-muted">
The max length of the text is 4000.
</small>
</div>
<button type="submit" class="btn btn-success">Post</button>
</form>
再利用可能なテンプレート
Bootstrapのフォームを作成することはできたが、毎回このコードを書くのは大変なので再利用可能にする。
まずtemplates
ディレクトリの中にincludes
ディレクトリを作成。
├── myproject/ │ ├── boards/ │ ├── db.sqlite3 │ ├── manage.py │ ├── myproject/ │ ├── static/ │ └── templates/ │ ├── base.html │ ├── home.html │ ├── includes/ # mkdirで作成 │ ├── new_topic.html │ └── topics.html ├── Pipfile └── Pipfile.lock
次にtemplates/includes/form.html
ファイルを作成し下記コードを記述する。
{% load widget_tweaks %} {% for field in form %} <div class="form-group"> {{ field.label_tag }} {% if form.is_bound %} {% if field.errors %} {% render_field field class="form-control is-invalid" %} {% for error in field.errors %} <div class="invalid-feedback"> {{ error }} </div> {% endfor %} {% else %} {% render_field field class="form-control is-valid" %} {% endif %} {% else %} {% render_field field class="form-control" %} {% endif %} {% if field.help_text %} <small class="form-text text-muted"> {{ field.help_text }} </small> {% endif %} </div> {% endfor %}
templates/new_topic.html
では別テンプレートを読み込む{% include %}
タグでform.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" novalidate> {% csrf_token %} {% include 'includes/form.html' %} <button type="submit" class="btn btn-success">Post</button> </form> {% endblock %}
これで今後は{% include 'includes/form.html' %}
と書くだけでテンプレートの再利用が可能になる。
フォームのテストを追加
boards/tests.py
にフォームのテストを追加する。
# ... other imports from .forms import NewTopicForm class NewTopicTests(TestCase): # ... other tests def test_contains_form(self): # <- new test url = reverse('new_topic', kwargs={'pk': 1}) response = self.client.get(url) form = response.context.get('form') self.assertIsInstance(form, NewTopicForm) def test_new_topic_invalid_post_data(self): # <- updated this one ''' 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, {}) form = response.context.get('form') self.assertEquals(response.status_code, 200) self.assertTrue(form.errors)
test_contains_form(self)
は新しいテストでresponse.context.get('form')
によりインスタンスを取得し、assertIsInstance
メソッドでNewTopicForm
のインスタンスか確認している。
test_new_topic_invalid_post_data(self)
は既存テストのアップデートで、データがない場合にバリデーションエラーが表示されるかself.assertTrue(form.errors)
により確認している。
テストコードを書いたらテストが通ることを確認しておく。
$ python manage.py test Creating test database for alias 'default'... System check identified no issues (0 silenced). ................ ---------------------------------------------------------------------- Ran 16 tests in 1.354s OK Destroying test database for alias 'default'...
(参考)django-widget-tweaksで発生したエラー
最初にdjango-widget-tweaks
でやったところTypeError
が発生した。バージョンは1.4.1。
str
を期待しているところでBoundField
のデータがあるのが問題のようだ。
調べたところDjango 2.0では下記リンクのコミットを適用する必要があるとのことで、django-widget-tweaks
をフォークしたdjango-widgets-improved
が紹介されている(django-widget-tweaks
をインストールしている場合はアンインストールすること)。
Always cast the result of render to a string · simhnna/django-widget-tweaks@a327bbe · GitHub
まとめ
django-widget-tweaks
はフォームをレンダリング時にCSSクラスなどを追加できる- Django 2.0では
django-widget-tweaks
をフォークしたdjango-widgets-improved
を使う {% load widget_tweaks %}
タグによりdjango-widget-tweaks
をロードする{% render_field field class="form-control" %}
のようにしてクラスを追加する{% include 'includes/form.html' %}
のようにしてテンプレートを再利用できる