В самом питоне с этим всё неплохо, есть отличный модуль pytz, а встроенный объект datetime корректно работает с часовыми поясами. Дело за малым — реализовать удобную обвязку. Первое что приходит в голову написать фильтр шаблона localtime и вызывать его таким образом:
{{ comment.time_added|localtime }} или
{{ comment.time_added|localtime|date:"d.m.Y" }}
Но сразу возникает пара проблем. Во-первых, функция фильтра не принимает контекст, а значит не сможет определить к какому часовому поясу приводить время. Во-вторых, текущий часовой пояс может пониматься по разному: браться из настроек текущего пользователя, определяться по выбранному городу или по IP. Т.е. то, что сейчас является локальным часовым поясом зависит от приложения и соответственно такой фильтр нужно будет постоянно переписывать. И в-третьих, все эти параметры нужно передавать в контекст.
Первая проблема решается использованием тега вместо фильтра (он может получать контекст), правда выглядеть это будет уже не так красиво:
{% localtime comment.time_added %} или
{% localtime comment.time_added as time_added %}{{ time_added|date:"d.m.Y" }}
Особо рисковые и нетерпеливые ценители красоты могут воспользоваться патчем, он как раз позволяет передавать контекст в фильтр.
Остались хлопоты с контекстом, можно написать свой context processor, а можно использовать стандартный django.core.context_processors.request и заполнять его свойство timezone с помощью Middleware:
class TimezoneMiddleware(object):
"""
Записывает в request свойство timezone
"""
def process_request(self, request):
assert hasattr(request, 'session'), "*.TimezoneMiddleware requires session middleware to be installed."
request.timezone = get_timezone(request)
request.session['timezone'] = request.timezone
return None
Зависимость от session middleware можно убрать, если вы не собираетесь кэшировать часовой пояс в сессии. Функция
get_timezone() будет зависеть от приложения и может выглядеть, например, так:def get_timezone(request):
# Конструируем по пользователю
if hasattr(request, 'user') and request.user.is_authenticated():
profile = request.user.get_profile()
if profile.timezone:
return profile.timezone
# Берём из сессии
if request.session.has_key('timezone'):
return request.session['timezone']
# Определяем город по IP, а по городу определяем часовой пояс
city_id = ip_to_city_id(request.META['REMOTE_ADDR'])
if city_id:
try:
city = City.objects.get(pk=city_id)
if city.timezone:
return city.timezone
except City.DoesNotExist:
pass
# Берём значение по умолчанию из настроек
return pytz.timezone(settings.FALLBACK_TIMEZONE)
Собственно, можно было бы привести код для тега и фильтра шаблона и на этом закруглится, но профессионально ленивый программист, вроде меня, решит, что писать тег или фильтр localtime каждый раз это хлопотно, плюс при выдаче форм нужно вручную преобразовывать время в полях туда и обратно, плюс в отсутствие контекста запроса (рассылка писем по крону, например) это без дополнительных телодвижений не заработает, плюс при работе в видах нужно быть постоянно начеку — календарик с событиями может выглядеть по-разному для разных часовых поясов. Что ж трудолюбивые ребята могут взять код фильтра в примере к вышеупомянутому патчу и быть таковы, остальные пусть будут готовы к небольшому колдовству.
Очевидно, если мы хотим, чтобы даты и времена автоматически переводились в текущий временной пояс, то без некоторой магии тут действительно не обойтись. Все данные из моделей мы получаем через их поля — отлично, преобразовывая время после выборки и перед вставкой можно получить требуемый эффект. Однако, поля ничего не знают ни о контексте шаблона, ни о объекте запроса, их вообще может не быть. Очевидно, активный часовой пояс должен быть глобальным. Можно посмотреть как аналогичная ситуация разрешена в django.utils.translation и реализовать то же для часовых поясов:
import pytz
from django.utils.thread_support import currentThread
_active = {}
def default_timezone():
"""
Возвращает часовой пояс сервера.
Функция подменяет себя во время первого вызова
"""
from django.conf import settings
_default_timezone = pytz.timezone(settings.TIME_ZONE)
global default_timezone
default_timezone = lambda: _default_timezone
return _default_timezone
def activate(tz):
if isinstance(tz, pytz.tzinfo.BaseTzInfo):
_active[currentThread()] = tz
else:
_active[currentThread()] = pytz.timezone(tz)
def deactivate():
global _active
if currentThread() in _active:
del _active[currentThread()]
def get_timezone():
tz = _active.get(currentThread(), None)
if tz is not None:
return tz
return default_timezone()
def to_active(dt):
tz = get_timezone()
if dt.tzinfo is None:
dt = default_timezone().localize(dt)
return dt.astimezone(tz)
def to_default(dt):
if dt.tzinfo is None:
return default_timezone().localize(dt)
else:
return dt.astimezone(default_timezone())
Функция
activate() устанавливают текущий часовой пояс, deactivate() возвращает пояс по-умолчанию. to_default() и to_active() преобразуют время к поясу сервера либо текущему. Осталось написать собственное поле модели:class TimezoneDateTimeField(models.DateTimeField):
__metaclass__ = models.SubfieldBase
def _to_python(self, value):
"""
Немагический метод преобразования дерьма в питоновый datetime
"""
return super(TimezoneDateTimeField, self).to_python(value)
def to_python(self, value):
"""
Метод преобразования дерьма в питоновый datetime.
Преобразовывает из времени сервера в текущий часовой пояс
"""
if value is None:
return value
return timezone.to_active(self._to_python(value))
def get_db_prep_value(self, value):
"""
Преобразовывает во время сервера для вставки в базу
"""
if value is not None:
value = timezone.to_default(self._to_python(value))
return connection.ops.value_to_db_datetime(value)
И устанавливать активный часовой пояс для каждого запроса, например, дописав
TimezoneMiddleware:class TimezoneMiddleware(object):
def process_request(self, request):
...
timezone.activate(request.timezone)
return None
def process_response(self, request, response):
timezone.deactivate()
return response
Готово, просто заменяем стандартный
DateTimeField на наше поле и время преобразовывается магически везде: и в шаблонах, и в формах, и в админке. Конечно, нет предела совершенству, можно реализовать своё поле формы для навешивания активного часового пояса на время, получаемое от пользователя, можно написать-таки фильтр для тех случаев когда используется сторонние приложения и их модели с не нашими полями. Что я и сделал в одном проекте о горнолыжном отдыхе, для которого и был написан весь этот код.Надеюсь, все кто дочитал до этого места получили или практическую пользу, или эстетическое удовольствие, которое, полагаю, многим пишущим на Django знакомо.
P. S. В недавно вышедшем Django 1.2 изменился интерфейс полей модели, поэтому приведённый код для
TimezoneDateTimeField понадобится допилить в соответствии с инструкцией по обновлению