はじめにの前に
フィルタの作り方を知りたい人はそこに至るまでが結構長く書いてあるので適当に飛んでいただいて結構です。
はじめに
テンプレートの中でモデルに記録されている日付が今日なのか判定する必要がありました。例えば以下のようなモデル。
class History(models.Model):
content = models.CharField(max_length=200)
date = models.DateTimeField()
これをビューで取得して、
def index(request):
histories = History.objects.all()
context = {
'histories': histories
}
return render(request, 'index.html', context)
テンプレートで表示、
<html>
<body>
<table>
{% for history in histories %}
<tr>
<td>{{ history.date }}</td>
<td>{{ history.content }}</td>
</tr>
{% endfor %}
</table>
</body>
</html>
で、history.date
が今日の場合に追加処理をしたいんだけど、今日ってどう判定すればいいの?というのが以下で述べるやり方を調べたきっかけです。
初めに考えた方法(今日を取得しておき比較)
初めに考えたのは以下の方法です。
- ビューで「今日」を取得し、
context
に設定しておく - テンプレートで「今日」と
history.date
を比較
つまり、
def index(request):
histories = History.objects.all()
today = timezone.now().date()
context = {
'histories': histories,
'today': today.strftime('%Y-%m-%d')
}
return render(request, 'today/index.html', context)
<table border="1">
{% for history in histories %}
<tr{% if history.date|date:"Y-m-d" == today %} class="today"{% endif %}>
<td>{{ history.date }}</td>
<td>{{ history.content }}</td>
</tr>
{% endfor %}
</table>
はっきり言って、いけてない。日付を文字列化しているのがダサすぎる。
また、比較のためにcontext
に「今日」を入れておかないといけないのもダサい。
余談1:ifの比較演算子
ところでいつの間にifで比較演算子が使えるようになったのだろう。元々は「ifは==で比較できないからifequal使わないといけないんすよね」と書こうと思って一応調べてみたら使えることがわかり逆にifequalはそのうちなくなるかもと書いてあった。少し前のDjangoを使ってる人は比較演算子使えないかもしれない。
余談2:日付のまま比較する
よく思うと日付を文字列化しなくても日付のまま比較できるんじゃないか?と試してみたらできました。問題はフィールドがdatetimeの場合にdateを取得している部分がダサいこと。
def index(request):
histories = History.objects.all()
today = timezone.now().date()
context = {
'histories': histories,
'today': today
}
return render(request, 'index.html', context)
<table border="1">
{% for history in histories %}
<tr{% if history.date.date == today %} class="today"{% endif %}>
<td>{{ history.date }}</td>
<td>{{ history.content }}</td>
</tr>
{% endfor %}
</table>
次に考えた方法(モデルにメソッドを作る)
というわけで初めに考えた方法は非常にいけてませんでした。復習しておくと、
-
context
に余計なものが入る - 日付が文字列になっている
- dateのまま比較もできるがフィールドがdatetimeだとdateを取り出しているところがダサい
というわけで次に考えた方法、モデルのことはモデルに聞け、つまり、モデルに「日付は今日か?」というメソッドを作ればいいのです。
class History(models.Model):
content = models.CharField(max_length=200)
date = models.DateTimeField()
def is_today(self):
date = self.date.date()
return date == date.today()
これを使うとテンプレートは、
<table border="1">
{% for history in histories %}
<tr{% if history.is_today %} class="today"{% endif %}>
<td>{{ history.date }}</td>
<td>{{ history.content }}</td>
</tr>
{% endfor %}
</table>
すっきり。context
に入れたtoday
も取り除けます。
多分最もいい方法(フィルタ)
というわけで二番目に考えた「モデルにメソッドを作る」は特に問題はないのですが、オブジェクト指向的な問題として、『そのモデルに「今日か?」と聞くのは適切なのか?』という問題があります。「履歴」というモデルに対して「今日か?」と聞くのは適切かということ。
より具体的に言うとこれを調べるきっかけとなった「あるデータを、うっかりミスで登録した場合にキャンセルできるようにしたい。ただし昔のデータもキャンセルされると整合性が取れなくなるので登録した当日限定でキャンセルできるようにする。さて、そんなデータに対して登録日が今日か?という処理の都合のようなメソッドを定義するのは適切か」という問題です。適切と言う人もいると思いますが私は適切に思いませんでした。
話が長くなりましたが、というわけで、モデルにメソッドを定義するのはいけてない(個人の感想です)ので、他の方法を考えたところ、フィルタを使うことを思いつきました。フィルタとは、先ほどもしれっとdateフィルタ使ってましたが、テンプレート中で使えてデータの変換を行うものです。それを使って、先にテンプレートを示すと、
<table border="1">
{% for history in histories %}
<tr{% if history.date|is_today %} class="today"{% endif %}>
<td>{{ history.date }}</td>
<td>{{ history.content }}</td>
</tr>
{% endfor %}
</table>
と書けて超いけてます(個人の感想です)
で、フィルタの作り方ですが、チュートリアルに書いてあります。要約すると、
- views.pyとかと同じディレクトリにtemplatetagsディレクトリを作る
- 作った後、サーバの再起動が必要
- フィルタを書くpyを作る。名前は自由に決められるが他と被らないように。テンプレートでのloadではこの名前を使用する
- モジュールレベルのregisterという変数を作り、フィルタを登録。デコレータとして使うこともできる
ということで具体的に先ほどのis_today
フィルタを書くと以下のようになります。
from django import template
import datetime
register = template.Library()
@register.filter(expects_localtime=True)
def is_today(value):
if type(value) is datetime.datetime:
value = value.date()
return value == value.today()
date、datetimeどちらでも使えるようにしてみました。なお、日付データを扱う場合は上記のようにexpects_localtime=True
を付けとくとタイムゾーンを考慮してくれるようです。
後は、テンプレートで{% load today_extra %}
と書けばis_today
フィルタが使えるようになります。