Есть несколько обыденных вещей, которые время от времени портят кровь нашему брату: падежи, числительные и часовые пояса, с проклятым переходом на летнее/зимнее время. Невольно позавидуешь китайцам у которых на всю страну всего один часовой пояс, а падежей нет и в помине. Будет совсем неплохо раз и навсегда разобраться с часовыми поясами и преобразованиями между ними хотя бы для Django-приложений.
В самом питоне с этим всё неплохо, есть отличный модуль 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:
| |
Зависимость от session middleware можно убрать, если вы не собираетесь кэшировать часовой пояс в сессии. Функция get_timezone() будет зависеть от приложения и может выглядеть, например, так:
| |
Собственно, можно было бы привести код для тега и фильтра шаблона и на этом закруглится, но профессионально ленивый программист, вроде меня, решит, что писать тег или фильтр localtime каждый раз это хлопотно, плюс при выдаче форм нужно вручную преобразовывать время в полях туда и обратно, плюс в отсутствие контекста запроса (рассылка писем по крону, например) это без дополнительных телодвижений не заработает, плюс при работе в видах нужно быть постоянно начеку — календарик с событиями может выглядеть по-разному для разных часовых поясов. Что ж трудолюбивые ребята могут взять код фильтра в примере к вышеупомянутому патчу и быть таковы, остальные пусть будут готовы к небольшому колдовству.
Очевидно, если мы хотим, чтобы даты и времена автоматически переводились в текущий временной пояс, то без некоторой магии тут действительно не обойтись. Все данные из моделей мы получаем через их поля — отлично, преобразовывая время после выборки и перед вставкой можно получить требуемый эффект. Однако, поля ничего не знают ни о контексте шаблона, ни о объекте запроса, их вообще может не быть. Очевидно, активный часовой пояс должен быть глобальным. Можно посмотреть как аналогичная ситуация разрешена в django.utils.translation и реализовать то же для часовых поясов:
| |
Функция activate() устанавливают текущий часовой пояс, deactivate() возвращает пояс по-умолчанию. to_default() и to_active() преобразуют время к поясу сервера либо текущему. Осталось написать собственное поле модели:
| |
И устанавливать активный часовой пояс для каждого запроса, например, дописав TimezoneMiddleware:
| |
Готово, просто заменяем стандартный DateTimeField на наше поле и время преобразовывается магически везде: и в шаблонах, и в формах, и в админке. Конечно, нет предела совершенству, можно реализовать своё поле формы для навешивания активного часового пояса на время, получаемое от пользователя, можно написать-таки фильтр для тех случаев когда используется сторонние приложения и их модели с не нашими полями. Что я и сделал в одном проекте о горнолыжном отдыхе, для которого и был написан весь этот код.
Надеюсь, все кто дочитал до этого места получили или практическую пользу, или эстетическое удовольствие, которое, полагаю, многим пишущим на Django знакомо.
P. S. В недавно вышедшем Django 1.2 изменился интерфейс полей модели, поэтому приведённый код для TimezoneDateTimeField понадобится допилить в соответствии с инструкцией по обновлению
Comments also at Хабр