Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
3
Help us understand the problem. What is going on with this article?
@junjis0203

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

More than 3 years have passed since last update.

はじめにの前に

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

はじめに

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

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

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

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

テンプレートで表示、

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を比較

つまり、

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)
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を取得している部分がダサいこと。

views.py
def index(request):
    histories = History.objects.all()
    today = timezone.now().date()
    context = {
        'histories': histories,
        'today': today
    }
    return render(request, 'index.html', context)
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を取り出しているところがダサい

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

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()

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

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

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>

と書けて超いけてます(個人の感想です)

で、フィルタの作り方ですが、チュートリアルに書いてあります。要約すると、

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

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

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フィルタが使えるようになります。

3
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
junjis0203
気になったことを徹底的に調べる探究者。そんな感じの調査レポートを書くことが多いですがどなたかの参考になりましたら幸いです。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
3
Help us understand the problem. What is going on with this article?