Djangoのテンプレートでモデルの日付が今日か判断する方法（三段階）

#はじめにの前に

フィルタの作り方を知りたい人はそこに至るまでが結構長く書いてあるので適当に飛んでいただいて結構です。

#はじめに

テンプレートの中でモデルに記録されている日付が今日なのか判定する必要がありました。例えば以下のようなモデル。

```py:models.py
class History(models.Model):
    content = models.CharField(max_length=200)
    date = models.DateTimeField()
```

これをビューで取得して、

```py:views.py
def index(request):
    histories = History.objects.all()
    context = {
        'histories': histories
    }
    return render(request, 'index.html', context)
```

テンプレートで表示、

```html:index.html
<html>
  <body>
    <table>
      {% for history in histories %}
      <tr>
        <td>{{ history.date }}</td>
        <td>{{ history.content }}</td>
      </tr>
      {% endfor %}
    </table>
  </body>
</html>
```

で、`history.date`が今日の場合に追加処理をしたいんだけど、今日ってどう判定すればいいの？というのが以下で述べるやり方を調べたきっかけです。

#初めに考えた方法（今日を取得しておき比較）

初めに考えたのは以下の方法です。

1. ビューで「今日」を取得し、`context`に設定しておく
2. テンプレートで「今日」と`history.date`を比較

つまり、

```py:views.py
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)
```

```html:index.html
<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を取得している部分がダサいこと。

```py:views.py
def index(request):
    histories = History.objects.all()
    today = timezone.now().date()
    context = {
        'histories': histories,
        'today': today
    }
    return render(request, 'index.html', context)
```

```html:index.html
<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を取り出しているところがダサい

というわけで次に考えた方法、モデルのことはモデルに聞け、つまり、モデルに「日付は今日か？」というメソッドを作ればいいのです。

```py:models.py
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()
```

これを使うとテンプレートは、

```html:index.html
<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フィルタ使ってましたが、テンプレート中で使えてデータの変換を行うものです。それを使って、先にテンプレートを示すと、

```html:index.html
<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>
```

と書けて超いけてます（個人の感想です）

で、フィルタの作り方ですが、[チュートリアル](https://docs.djangoproject.com/ja/1.10/howto/custom-template-tags/)に書いてあります。要約すると、

- views.pyとかと同じディレクトリにtemplatetagsディレクトリを作る
  - 作った後、サーバの再起動が必要
- フィルタを書くpyを作る。名前は自由に決められるが他と被らないように。テンプレートでのloadではこの名前を使用する
- モジュールレベルのregisterという変数を作り、フィルタを登録。デコレータとして使うこともできる

ということで具体的に先ほどの`is_today`フィルタを書くと以下のようになります。

```py:templatetags/today_extra.py
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`フィルタが使えるようになります。