2013年9月2日月曜日

[Django 1.5.1] TypeError: can't compare offset-naive and offset-aware datetimes

TimeZoneは後から真面目に考えようと、とりあえず何も考えずに実装していたら、日付を比較する行でエラーが発生!
class Permit(models.Model):
    ...
    expires = models.DateTimeField()
    created = models.DateTimeField(auto_now_add=True)
    ...
    permit = Permit.object.get(id=xxx)
    if permit.expires < datetime.now():  # ERROR
        ...
naiveとawareの意味が判らないので、まずは、そこから調査。
Pythonの日付処理とTimeZoneによると、TimeZone情報を持ったdatetimeオブジェクトがaware, 持たない場合がnaive。awareとnaiveで比較するとエラーになることが判明。

次にDjangoだが、settings.pyにTimeZoneの有効/無効の設定があり、デフォルトは有効になっている
...
# If you set this to False, Django will not use timezone-aware datetimes.
USE_TZ = True
...

では、実際に、それぞれのFieldにどんな値が設定されているのか確認すると expiresは日本時間がUTCとして、createdは正しい現在時刻がUTCとして設定されていた。
そこで、以下の2ヶ所を修正をして、期待通りの動作になった。

まず、expiresに値を設定するところでTimeZoneを指定するようにした。
class Permit(models.Model):
    ...
    def save(self, *args, **kwargs):
        dt = datetime.strptime(xxx)
        self.expires = dt.replace(tzinfo=pytz.timezone('Asia/Tokyo'))
        ...

次に時間を比較するところで、UTCを使用するように変更
import pytz
...
    permit = Permit.objects.get(id=xxx)
    if permit.expires < datetime.utcnow().replace(tzinfo=pytz.UTC):
        ...

もっと、よく調べたら、Djangoに便利なユーティリティーが存在することが判明
from django.utils import timezone
...
    permit = Permit.objects.get(id=xxx)
    if permit.expires < timezone.now():
        ...