Djangoで、タイムゾーンの変換

Twitterでシェア FaceBookでシェア はてなブックマークでシェア

Python - Django
2018年11月22日21:15に更新(約21日前)
2018年11月18日5:07に作成(約25日前)

旧ブログ移行記事です。

概要

以前に、pytzを使ったタイムゾーンの変換を紹介しました。Djangoはフレームワークのレベルでタイムゾーンをサポートしており、利用者は簡単にそれを利用することができます。

タイムゾーンを有効にする

settings.pyを編集します。

# UTCから変更します。日本標準時
TIME_ZONE = 'Asia/Tokyo'

# デフォルトTrue。Falseにはしないように!
USE_TZ = True

これは以下の動作をします。

  1. テンプレートに渡した日付は、自動で日本時間に変換される
  2. フォームの日付入力欄(管理画面含む)で、入力した日付は日本時間として処理される

もう少し詳細を話すならば、

1の具体的なものは、フォームとモデルのDatetimeFieldと、django.utils.timezone.now()での日付が該当します。内部的にはawareなdatetimeオブジェクトであれば全て変換するので、自分で作ったタイムゾーン情報尽きのdatetimeオブジェクトも変換されることになります。

2ですが、これはフォームから送信された文字列をdatetime.strptime()datetimeオブジェクトに変換し、pytz.timezone(settings.TIME_ZONE).localize(dt)としていると考えましょう。

とにかく、TIME_ZONEで指定したもので日付を扱えるということです。

他のタイムゾーンに変更する

これは全く難しくありません。TIME_ZONE = 'Asia/Tokyo'という指定は、デフォルトの表示として日本時間を使いますという指定に過ぎません。USE_TZ = Trueにした段階で、Djangoは全ての日付(datetimeオブジェクト)をUTCでデータベースに保存します。

世界の基準であるUTCで保存するということは、他の全てのタイムゾーンにいつでも変更できるということです。settings.pyTIME_ZONEを、US/Eastern(東部標準時、ニューヨーク等)に変更すると、全ての日付はニューヨークでの時間に変換されます。

複数のタイムゾーンで同時に表示する

いちいちsettings.pyを修正するのは効率が悪いですし、同時に複数のタイムゾーンで表示したい場合はどうでしょうか。この場合は少し作業が必要ですが、そんなに難しくありません。試しに、現在日付を多くのタイムゾーンで表示してみます。

まずviews.py

from django.shortcuts import render
from django.utils import timezone
import pytz


def top(request):
    context = {
        'now': timezone.now(),
        'timezones': pytz.common_timezones,
    }
    return render(request, 'app/top.html', context)

timezone.now()は、よく目にする現在の時間の取得処理ですね。pytz.common_timezonesは、タイムゾーンの名前を一覧取得します。Asia/Tokyoとか、US/Easternとか、Europe/Parisとか、そういった文字列の詰まったリストです。

次はtop.htmlです。

{% extends 'app/base.html' %}
{% load tz %}

{% block content %}
{% for tz in timezones %}
  {{ tz }}: {{ now|timezone:tz }}
  <hr>
{% endfor %}

{% endblock %}

{% load tz %}は、Djangoのタイムゾーン関連のテンプレートフィルタ、テンプレートタグを利用するのに使います。{% for tz in timezones %}で各タイムゾーン名を取り出し、タイムゾーン名: そのタイムゾーンでの現在日付・時刻 として表示させています。{{ now|timezone:tz }}のnowはビューで取得した現在の日付時刻で、timezoneフィルタにforループで取得しているタイムゾーン名(tz)を渡し、変換させるといった流れです。

pytz.common_timezonesだと多すぎる、一部のタイムゾーンだけで充分だ、ということならば以下のように直接書いてもいいですし...

日本: {{ now|timezone:'Asia/Tokyo' }}<hr>
ニューヨーク: {{ now|timezone:'US/Eastern' }}<hr>
パリ: {{ now|timezone:'Europe/Paris' }}<hr>

'timezones': ['Asia/Tokyo', 'US/Eastern', 'Europe/Paris'],のように自分でタイムゾーンが集まったリスト等を作って、それを今回のようにforで取り出してもよいでしょう。

モデルのDateTimeFieldを複数ゾーンで表示

モデル名がPost、DateTimeFieldのフィールド名がcreated_atとするならば、{{ tz }}: {{ post.created_at|timezone:tz }}のようにするだけです。当然ですが、テンプレートにモデルインスタンス、又はQueryset(モデル.objects.all()とかfilter()が返すやつ)を渡すのも、忘れないようにしましょう。

ビューで作ったdatetimeオブジェクトを複数ゾーンで表示

timezone.now()による現在ではなく、特定の日付時刻...つまりdatetime.datetime(year=...)を変換したい場合はどうでしょうか。ちょっと間違えやすい処理なので紹介しておきます。settings.pyTIME_ZONEAsia/Tokyoだったとしましょう。

日本時間で2018/12/24 0時の場合です。

import datetime
from django.shortcuts import render
from django.utils import timezone
import pytz


def top(request):
    context = {
        'dt': timezone.make_aware(datetime.datetime(year=2018, month=12, day=24, hour=0)),
        'timezones': pytz.common_timezones,
    }
    return render(request, 'app/top.html', context)

timezone.make_aware()に、datetimeオブジェクトを渡します。以前のpytzを使ったタイムゾーンの変換の記事を見た方ならば、make_aware()の内部でlocalize()を呼んでいるんだなと察しがつくかもしれません。実際そうです。

ニューヨークで2018/12/24 0時の場合です。こっちはちょっと難しいです。

import datetime
from django.shortcuts import render
from django.utils import timezone
import pytz


def top(request):
    ust = pytz.timezone('US/Eastern')
    context = {
        'dt': timezone.make_aware(datetime.datetime(year=2018, month=12, day=24, hour=0), timezone=ust),
        'timezones': pytz.common_timezones,
    }
    return render(request, 'app/top.html', context)

make_aware()は、デフォルトでカレントタイムゾーンを使います。これはsettings.TIME_ZONEのことだと思っても今は差し支えありません。Asia/Tokyoにしているという前提でしたね。今回のようにsettings.TIME_ZONEとは違うタイムゾーンでの日付を与える場合は、それをmake_aware()timezone引数に指定する必要があります。

これと関連して、フォーム(モデルフォーム含む)のDatetimeFieldにも注意が必要です。これは日付の入力欄をHTML上に作成しますが、そこで入力された日付のタイムゾーンはsettings.TIME_ZONEで処理されます。具体的なケースを挙げると、アメリカの方々向けのサイトを運営していたとしましょう。アメリカと言っても西と東で3時間の時差があります。

運営者は律儀なので、各地域に合わせて時間を表示したとします。テンプレートで以下のように書けるでしょう。

EST(ニューヨーク、ワシントン等): {{ obj.created_at|timezone:'US/Eastern' }}<hr>
CST(シカゴ、ヒューストン等): {{ obj.created_at|timezone:'US/Central' }}<hr>
MST(フェニックス、デンバー等): {{ obj.created_at|timezone:'US/Mountain' }}<hr>
PST(ロサンゼルス、サンフランシスコ等): {{ obj.created_at|timezone:'US/Pacific' }}<hr>

表示に関しては問題なさそうです。Django・pytzのタイムゾーンのサポートは万全で、サマータイムにも関してもきちんと表示します。

しかし、ユーザーが日付を入力する際はちょっと問題です。settings.TIME_ZONEがUS/Easternだったとして、ニューヨークの人は素直に入力できますが、他3地域はその地域の時間を変換して入力しなければなりません。これに対する最も簡単な方法は、ユーザー毎にタイムゾーン情報を持たせることです。次で説明します。

ユーザーにタイムゾーンを選ばせる

少し進んだ機能として、ユーザーがタイムゾーンを自由に切り替えれるようなアプリケーションを作ってみましょう。

こんな感じで、タイムゾーンの設定ページがあります。現在のタイムゾーン名も表示しました。 現在はAsia/Tokyoです。

多種多様なタイムゾーンがありますね。

US/Easternに設定しました。現在のタイムゾーン名も変わりましたね。

管理画面の新規作成ページです。日付欄の初期値にtimezone.nowとしており、先ほど設定したUS/Eastern(ニューヨークとか)での現在の時間が入っています。

タイムゾーンの設定ページでAsia/Tokyoに戻した後、さきほどのモデルの更新画面に行くと、日本時間にちゃんと修正されました。

設定ページでAsia/Tokyoに設定したならば日本での時間を入力できますし、US/Easternに設定していたらニューヨーク時間で入力できます。そして、それらを自在に切り替えれます!

これらの設定はあくまでユーザー毎(正確にはブラウザ毎)に反映されるので、その点も安心です。管理画面上で確認していますが、勿論自分で作ったページでも同様に変換がされます。

まず、views.pyにタイムゾーン設定ページのビューを作成します。

def set_timezone(request):
    if request.method == 'POST':
        request.session['django_timezone'] = request.POST['timezone']
        return redirect('app:top')  # トップページへリダイレクト
    else:
        return render(request, 'app/timezone.html', {'timezones': pytz.common_timezones})

request.method == 'POST'は、早い話が設定ページのボタンを押したときです。セッションに選択したタイムゾーンを保存しておきます。GETの際...つまり普通に設定ページを開いた場合は、タイムゾーン設定用のページを表示します。テンプレートへは、{'timezones': pytz.common_timezones}としてタイムゾーン名の一覧を渡します。

timezone.htmlは、少々複雑です。

{% extends 'app/base.html' %}
{% block content %}
  {% load tz %}
  {% get_current_timezone as TIME_ZONE %}

  <h1>タイムゾーンの選択</h1>
  <p>現在のタイムゾーンは、{{ TIME_ZONE }} です。</p>
  <form action="{% url 'app:set_timezone' %}" method="POST">
    {% csrf_token %}
    <label for="timezone">Time zone:</label>
    <select name="timezone">
      {% for tz in timezones %}
        <option value="{{ tz }}"{% if tz == TIME_ZONE %} selected{% endif %}>{{ tz }}</option>
      {% endfor %}
    </select>
    <input type="submit" value="Set">
  </form>
{% endblock %}

{% load tz %}は、タイムゾーン関連のテンプレートフィルタ・タグを使うのに必要です。さっきもちらっと説明しました。{% get_current_timezone as TIME_ZONE %}は、現在のカレントタイムゾーンを取得します。デフォルトではsettings.TIME_ZONEを、ユーザーがタイムゾーンを設定していればばそのタイムゾーン名が返されます。

後はビューから渡されたタイムゾーン一覧を、<option value="{{ tz }}"{% if tz == TIME_ZONE %} selected{% endif %}>{{ tz }}</option>としていきます。例えば<option value="Asia/Tokyo">Asia/Tokyo</option>のようになります。{% if tz == TIME_ZONE %} selected{% endif %}の部分は、カレントタイムゾーンだった場合は選択済みにするということです。

このビューとテンプレートがやっているのは、ユーザーが選択したタイムゾーンをセッションに保存する、ということです。そのタイムゾーンをDjangoに適用するには、別の処理をする必要があります。それをするために、Djangoのミドルウェアを作成する必要があります。

まずsettings.pyMIDDLEWAREを編集します。

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'app.middleware.timezone_middleware',  # 追加!
]

appディレクトリ内に、middleware.pyを作ります。そして、以下のようにします。

from django.utils import timezone
import pytz


def timezone_middleware(get_response):

    def middleware(request):
        tzname = request.session.get('django_timezone')
        if tzname:
            timezone.activate(pytz.timezone(tzname))
        else:
            timezone.deactivate()
        response = get_response(request)
        return response

    return middleware

Djangoのミドルウェアを使うことで、ビューが呼び出される前後に処理を書くことができます。色々な書き方や機能があるのですが、今回やっているのはビューが呼び出される前の段階でユーザーが選択したタイムゾーンをDjangoに適用するという処理です。

tzname = request.session.get('django_timezone')は、ユーザーが設定したタイムゾーン情報を取得しています。タイムゾーンの設定ページでAsia/Tokyo を選択していれば、それはビューでセッションにセットされ、このミドルウェアでそれが取得できます。

timezone.activate(pytz.timezone(tzname))は、その取得したタイムゾーンをDjangoに実際に適用する処理です。イメージとしては、settings.TIME_ZONEの値に代入していると思ってください(実際にはスレッド毎にタイムゾーンを持たせる必要があるので、もう少し違う形です)。

これはセッションを使っていますが、やろうと思えばデータベースでタイムゾーン名を管理することもできるでしょう。

Twitterでシェア FaceBookでシェア はてなブックマークでシェア

記事にコメントする