2-12. タイムゾーンと日時オブジェクトを扱う

今回のテーマは「タイムゾーンと日時オブジェクトを扱う」です。いよいよ第二章も最終回です。もうDjnagoの特徴や使い方が大分見えてきましたよね。今回はデータベースに保存されている日時を利用して現在時刻との差分を計算していきます。今回扱う範囲は公式ドキュメントのタイムゾーンに詳細説明があります。

日時オブジェクトのnativeとawareについて

settings.pyにてUSE_TZ = Trueにしている場合タイムゾーンがサポートされます。この状態ではDjangoはタイムゾーンを認識する日時オブジェクト(awareな日時オブジェクト)を使用します。本サイトのようにstartprojectコマンドによってプロジェクトを生成した場合は初期設定としてUSE_TZ=Trueの設定になっています。よって現状ではawareな日時オブジェクトを使用してきました。タイムゾーンが有効となっている場合、データベースにはUTCで日時を保存しています。Djangoはテンプレートやフォーム等で表示する際に、設定されたタイムゾーンで変換をしています。

現在時刻の取得に関して

pythonで現在時刻を取得する際にdatetime.datetime.now()と覚えている方も多いのではないでしょうか?この方法ではnativeの日時オブジェクトが取得されるため、awareな日時オブジェクトとの比較は出来ません。データベースから取得した日時オブジェクトと現在時刻を比較するにはどうしたら良いのでしょうか?Djangoにはdjango.utils.timezoneモジュールがあります。このモジュールのnow関数を使うことで現時刻の日時オブジェクトを適当なモードで取得することが出来ます。now関数の中身を見てみましょう。

django/utils/timezone.py


def now():
    """
    Return an aware or naive datetime.datetime, depending on settings.USE_TZ.
    """
    if settings.USE_TZ:
        # timeit shows that datetime.now(tz=utc) is 24% slower
        return datetime.utcnow().replace(tzinfo=utc)
    else:
        return datetime.now()

このようにUSE_TZによって返すオブジェクトを変えています。タイムゾーンが無効(USE_TZ=False)の場合にはnativeの日時オブジェクトが返されるのが理解できると思います。

よってDjango内で現在時刻を取得する際にはtimezoneモジュールをインポートしてnow関数を呼び出すのが良いでしょう。

データベース

簡単にデータベースについて触れておきたいと思います。筆者としてはタイムゾーンは常に有効にして使用した方が良いと考えていますが、タイムゾーンの有効・無効を切り替える場合もあると思います。PostgreSQLはタイムゾーン情報をデータベースに保存しているために、タイムゾーンの有効・無効は自由に切り替えられます。しかしそれ以外のデータベースに関してはタイムゾーンがを無効に切り替えた場合にはUTCからネイティブなdatetimeに変換する必要があります。(参考:公式ドキュメント

現在時刻との差分を計算してNEWラベルをつける

さて、今回は演習として一時間以内に新しく作成されたトピックはトップページにNEWラベルを表示するようにしましょう。トップページを表示しているビューはbase/views.pyのTopicListViewクラスですので、ここに手を加えていきます。

base/views.py(一部抜粋)


from django.utils import timezone

class TopicListView(ListView):
    template_name = 'base/top.html'
    # model = Topic
    queryset = Topic.objects.order_by('-created')
    context_object_name = 'topic_list'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.new_list = []

    def get_queryset(self):
        topic_list = Topic.objects.order_by('-created')
        self.new_list = self._make_new_list(topic_list)
        return topic_list

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['new_list'] = self.new_list
        return ctx

    def _make_new_list(self, topic_list):
        def pickup_topic(topic):
            now = timezone.now()
            diff = (now - topic.created).total_seconds() / (60 * 60)
            if diff > 1:
                return False
            else:
                return True
        return list(map(lambda x : x.id ,filter(pickup_topic, topic_list)))

解説は不要かと思いますが、現在時刻をtimezone.now()で取得してデータベースに格納されているtopic.createdと差を求めて秒を時間に変換しています。この時間が1時間以下のトピックのIDのみのリストを作成して返していますね。コンテキストにnew_listを渡すためnew_listをインスタンス変数として渡しています。

ラベルを表示するようにテンプレートも変更しましょう。

templates/base/top.html(一部抜粋)



{% extends 'base/base.html' %}
{% block title %}ITについて切磋琢磨する掲示板 - {{ block.super }}{% endblock %}
{% block content %}
<div class="ui grid stackable">
    <div class="eleven wide column">
        <div class="ui breadcrumb">
            <a class="active section">TOP</a>
        </div>
        <div class="ui segment">
            <div class="content">
                <div class="header"><h3>新着スレッド</h3></div>
                <div class="ui divided items">
                    {% for topic in topic_list %}
                    <div class="item">
                        <div class="content">
                            <div class="header">
                                <a href="{% url 'thread:topic' pk=topic.id %}">
                                    <h4>
                                        {% if topic.id in new_list %}
                                        <div class="ui violet horizontal label">new</div>
                                        {% endif %}
                                        {{topic.title}}
                                    </h4>
                                </a>
                            </div>
                            <div class="meta">
                                <span class="name">{{topic.user_name}}</span>
                                <span class="date">{{topic.created}}</span>
                            </div>
                        </div>
                    </div>
                    {% endfor %}
                </div>
            </div>
        </div>
    </div>
    {% include 'base/sidebar.html' %}
</div>
{% endblock %}

全て掲載しましたが、修正点はトピックタイトルの部分のみです。

では確認してみましょう。新規にトピックを作成するとNEWラベルがつきますね。

最後に

ここまで、Djangoの機能をつまみ食いしながら紹介してきました。少々無理のある機能もあり、掲示板というお題で始めてしまってよかったのか悩む場面もありましたが、何とかそれなりの機能を有した掲示板になってきたのではないでしょうか?次章からはDjangoの認証機能を利用して活きます。ここまで作成した掲示板を会員サイトに修正していく予定です。好ご期待!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です