概要
Djangoフレームワークのテンプレート(HTML)側を実装していると、Jinjaテンプレートでは実装できたのに
Djangoのテンプレートだと実装できないフィルタが存在した。
そんなとき、Djangoテンプレートをカスタマイズして対応できたので、その作成方法についてまとめた。
前提
以下のタスク編集画面の入力チェックエラー時、画面入力内容を保持するためにカスタムフィルタを作成した。
概要 クラスベースビュー(class-based view)でタスク更新機能を作成したのでまとめた。 タスク更新機能は、Djangoがもともと用意しているUpdateViewクラスを継承して作成する。 UpdateViewをどのよ[…]
経緯
タスク編集画面にて、初期表示は問題なく実現できるが入力チェックエラーとなった場合に以下の項目の入力内容がクリアされた。
・タスク期日
・カテゴリ
・ステータス
実際の画面の動き
初期表示
問題となる挙動
編集画面で入力チェックエラーを起こすと、「期日」「カテゴリ」「ステータス」の入力値がクリアされる。
編集画面テンプレート
templates/todo/edit.html
(略)
<div class="card-body">
<form method="post">
{% csrf_token %}
<div class="form-group">
<label for="task">タスク<span class="text-primary ml-3">※必須</span></label>
<input type="text" class="form-control" id="task" name="task" value="{{ form.task.value }}">
</div>
{% for error in form.task.errors %}
<div id="title-error" class="text-danger mb-3 error-msg">
{{ error }}
</div>
{% endfor %}
<div class="form-group">
<label for="memo">メモ</label>
<textarea class="form-control" id="memo" name="memo" rows="3">{{ form.memo.value}}</textarea>
</div>
{% for error in form.memo.errors %}
<div id="title-error" class="text-danger mb-3 error-msg">
{{ error }}
</div>
{% endfor %}
<div class="form-group">
<label for="due_date">期日<span class="text-primary ml-3">※必須</span></label>
<input type="date" class="form-control" id="due_date" name="due_date" value="{{ form.due_date.value|date:'Y-m-d' }}">
</div>
{% for error in form.due_date.errors %}
<div id="title-error" class="text-danger mb-3 error-msg">
{{ error }}
</div>
{% endfor %}
<div class="form-group">
<label for="category">カテゴリ<span class="text-primary ml-3">※必須</span></label>
<select class="form-control" id="category" name="category">
<option value="">選択してください</option>
{% for category in categories %}
<option value="{{ category.id }}" {% if category.id == form.category.value %}selected{% endif %}>{{ category.category_name }}</option>
{% endfor %}
</select>
</div>
{% for error in form.category.errors %}
<div id="title-error" class="text-danger mb-3 error-msg">
{{ error }}
</div>
{% endfor %}
<div class="form-group">
<label for="status">ステータス<span class="text-primary ml-3">※必須</span></label>
<select class="form-control" id="status" name="status">
<option value="">選択してください</option>
<option value="0" {% if form.status.value == 0 %}selected{% endif %}>未完了</option>
<option value="1" {% if form.status.value == 1 %}selected{% endif %}>完了</option>
</select>
</div>
{% for error in form.status.errors %}
<div id="title-error" class="text-danger mb-3 error-msg">
{{ error }}
</div>
{% endfor %}
<button type="submit" class="btn btn-primary mt-3">更新</button>
</form>
</div>
(略)
原因
初期表示時点のデータと、入力チェックエラーとなって画面に返却されるデータの型が異なるため。
詳細
初期表示時
入力チェックエラー時
タスク名の入力フィールドを空にしてエラーを発生させる。
問題点
初期値と入力チェックエラー後で、フィールドの値のデータ型が変わってしまうため、画面入力値保持のロジックが機能していない。
画面入力値保持の方法
タスク期日
<input id="due_date" class="form-control" name="due_date" type="date" value="{{ form.due_date.value|date:'Y-m-d' }}" />
{{ form.due_date.value|date:'Y-m-d' }}
dateフィルタは、日付オブジェクトを指定した形式の文字列に変換するフィルタになる。
そのため、入力チェックエラー後の文字列が返却されると日付オブジェクトではないため、上記のフィルタを通らない。
カテゴリ
<select class="form-control" id="category" name="category">
<option value="">選択してください</option>
{% for category in categories %}
<option value="{{ category.id }}" {% if category.id == form.category.value %}selected{% endif %}>{{ category.category_name }}</option>
{% endfor %}
</select>
<option value="{{ category.id }}" {% if category.id == form.category.value %}selected{% endif %}>{{ category.category_name }}</option>
category.idはDBから取得した一覧のデータのため、int型になる。
form.category.valueは、初期表示時点だとint型のためcategory.idとif文比較ができるが、入力チェックエラー後の文字列だとint型と比較できない。
そのため、セレクトボックスが選択状態にならない。
ステータス
<select class="form-control" id="status" name="status">
<option value="">選択してください</option>
<option value="0" {% if form.status.value == 0 %}selected{% endif %}>未完了</option>
<option value="1" {% if form.status.value == 1 %}selected{% endif %}>完了</option>
</select>
<option value="0" {% if form.status.value == 0 %}selected{% endif %}>未完了</option>
<option value="1" {% if form.status.value == 1 %}selected{% endif %}>完了</option>
カテゴリと事象は同じ。
初期表示時は、form.status.valueはint型のためif条件比較が機能するが、入力チェックエラー後の文字列だと比較できないため「選択してください」しか表示されない。
対応
作成したフィルタを問題となっているフィールドに適応させて、初期値も入力エラー時もデータを文字列に統一することで条件文が適用できないケースを回避する。
テンプレートファイル構成
カスタムテンプレートはtemplatetagsフォルダにファイルを格納するルールがある。
HTMLテンプレートを格納しているフォルダと同じ階層に、カスタムフィルタを格納するフォルダを作成する。
Djangoでは各アプリにtemplatetagsフォルダが存在すると、自動的に参照してくれるようになるらしい。
そのため、settings.pyなどでDjango側にファイルやフォルダの存在を検知してもらう設定は不要。
テンプレートフィルタの作成
templatetags/todo_template_filter.py
from django import template
from datetime import datetime
register = template.Library()
@register.filter
def to_date(val):
"""日付オブジェクトを文字列に変換します。"""
if isinstance(val, datetime):
return f"{val:%Y-%m-%d}"
return val
@register.filter
def to_string(val):
"""任意の値を文字列に変換します。"""
return str(val)
from django import template
register = template.Library()
@register.filter
templateモジュールをインポートして、template.Libraryオブジェクトを作成する。
あとは自作したフィルタ関数に@register.filterデコレータをつけるだけ。
@register.filter
def to_date(val):
"""日付オブジェクトを文字列に変換します。"""
if isinstance(val, datetime):
return f"{val:%Y-%m-%d}"
return val
valがdatetimeオブジェクトの場合、datetimeオブジェクトを「yyyy-mm-dd」形式に変換して返却する。
datetimeオブジェクト以外の場合、そのままvalを返却する。
これにより、日付型オブジェクトでも文字列オブジェクトでもどちらのパターンも同じ「yyyy-mm-dd」の文字列になる。
@register.filter
def to_string(val):
"""任意の値を文字列に変換します。"""
return str(val)
valを文字列に変換して返却する。
テンプレートフィルタの使用方法
カスタムフィルタのロード
自作したテンプレートフィルタを参照できるように、loadを使用して対象のファイルを参照させる。
{% extends "base.html" %}
{% load todo_template_filter %}
上記をedit.htmlの冒頭に記述した。
フィルタの適用方法
フィルタを適用したい場合、{{ 対象のデータ | フィルタメソッド名 }} と記述をする。
上記のルールで各項目にカスタムフィルタを適用させる。
<input type="date" class="form-control" id="due_date" name="due_date" value="{{ form.due_date.value|to_date }}">
作成したto_dateメソッドを使用している。
上記により、form.due_date.valueが日付オブジェクトの場合、「yyyy-mm-dd」形式の文字列に変換される。
また、更新リクエストを送信してエラーとなる場合でも、「yyyy-mm-dd」形式の文字列が返却されるだけ。
文字列が返却された場合、to_dateはそのまま文字列を返却する。
<select class="form-control" id="category" name="category">
<option value="">選択してください</option>
{% for category in categories %}
<option value="{{ category.id }}" {% if category.id|to_string == form.category.value|to_string %}selected{% endif %}>{{ category.category_name }}</option>
{% endfor %}
</select>
上記は「カテゴリID」と「form.category.value」のどちらもto_stringフィルタを通することで文字列に変換している。
初期表示の場合、「form.category.value」はint型で出力予定だが、to_stringをはさむことで文字列になる。
これにより、初期表示でもエラー時でも文字列に統一されるため、文字列比較の条件文がいつでも機能する。
<select class="form-control" id="status" name="status">
<option value="">選択してください</option>
<option value="0" {% if form.status.value|to_string == '0' %}selected{% endif %}>未完了</option>
<option value="1" {% if form.status.value|to_string == '1' %}selected{% endif %}>完了</option>
</select>
ステータスも文字列に変換して、文字列の数字と比較するよう修正した。
検証
以上の対応で、入力チェックエラー時でもフィールドがクリアされることがなくなる。
初期表示
入力チェックエラー時でもクリアされない