Spec-Zone.ru › Django 4.1

Часовые пояса

Обзор

Когда поддержка часовых поясов включена, Django хранит информацию о датах и временах в формате UTC в базе данных, использует объекты datetime с учетом часового пояса во внутренних операциях и переводит их в часовой пояс конечного пользователя в шаблонах и формах.

Это удобно, если ваши пользователи проживают в нескольких часовых поясах, и вы хотите отображать информацию о датах и временах в соответствии с местным временем каждого пользователя.

Даже если ваш сайт доступен только в одном часовом поясе, всё равно рекомендуется хранить данные в формате UTC в базе данных. Основная причина – летнее время (DST). Многие страны используют систему DST, где время переводится вперёд весной и назад осенью. Если вы работаете с местным временем, вы, вероятно, столкнетесь с ошибками дважды в год, когда происходят переходы. Это, вероятно, не имеет значения для вашего блога, но это проблема, если вы пересчитываете или недосчитываете плату своим клиентам на один час дважды в год, каждый год. Решением этой проблемы является использование UTC в коде и использование местного времени только при взаимодействии с конечными пользователями.

Поддержка часовых поясов отключена по умолчанию. Чтобы включить её, установите USE_TZ = True в вашем файле настроек.

Примечание

В Django 5.0 поддержка часовых поясов будет включена по умолчанию.

Поддержка часовых поясов использует zoneinfo, которая является частью стандартной библиотеки Python с Python 3.9. Пакет backports.zoneinfo автоматически устанавливается вместе с Django, если вы используете Python 3.8.

Изменено в Django 4.0:

zoneinfo стал реализацией по умолчанию для часовых поясов. Вы можете продолжать использовать pytz в течение цикла релизов 4.x с помощью настройки USE_DEPRECATED_PYTZ.

Примечание

Создаваемый по умолчанию settings.py файл django-admin startproject включает USE_TZ = True для удобства.

Если у вас возникли проблемы, начните с часто задаваемых вопросов о часовых поясах.

Концепции

Наивные и осознанные объекты datetime

Объекты datetime.datetime Python имеют атрибут tzinfo, который может использоваться для хранения информации о часовом поясе, представленной экземпляром подкласса datetime.tzinfo. Если этот атрибут установлен и описывает смещение, объект datetime является осознанным. В противном случае он наивный.

Вы можете использовать is_aware() и is_naive() для определения того, являются ли datetime осознанными или наивными.

Когда поддержка часовых поясов отключена, Django использует наивные объекты datetime в местном времени. Это достаточно для многих случаев использования. В этом режиме для получения текущего времени вы бы написали:

import datetime

now = datetime.datetime.now()

Когда поддержка часовых поясов включена (USE_TZ=True), Django использует осознанные объекты datetime с учетом часового пояса. Если ваш код создаёт объекты datetime, они также должны быть осознанными. В этом режиме приведенный выше пример становится:

from django.utils import timezone

now = timezone.now()

Предупреждение

Работа с осознанными объектами datetime не всегда интуитивно понятна. Например, аргумент tzinfo стандартного конструктора datetime не работает надёжно для часовых поясов с летним временем. Использование UTC обычно безопасно; если вы используете другие часовые пояса, вы должны внимательно изучить документацию zoneinfo.

Примечание

Объекты datetime.time Python также имеют атрибут tzinfo, а PostgreSQL имеет соответствующий тип time with time zone. Однако, как указано в документации PostgreSQL, этот тип «обладает свойствами, которые ведут к сомнительной полезности».

Django поддерживает только наивные объекты time и будет генерировать исключение, если вы попытаетесь сохранить осознанный объект time, поскольку часовой пояс для времени без связанной даты не имеет смысла.

Интерпретация наивных объектов datetime

Когда USE_TZ равно True, Django всё ещё принимает наивные объекты datetime, чтобы сохранить обратную совместимость. Когда слой базы данных получает такой объект, он пытается сделать его осознанным, интерпретируя его в по умолчанию часовом поясе и генерирует предупреждение.

К сожалению, во время перехода на летнее время некоторые даты и времена не существуют или являются неоднозначными. Поэтому вы всегда должны создавать осознанные объекты datetime, когда включена поддержка часовых поясов. (См. Using ZoneInfo section of the zoneinfo docs для примеров использования атрибута fold для указания смещения, которое должно применяться к datetime во время перехода на летнее время.)

На практике это редко вызывает проблемы. Django предоставляет вам осознанные объекты datetime в моделях и формах, а в большинстве случаев новые объекты datetime создаются из существующих с помощью арифметики timedelta. Единственное время, которое часто создаётся в прикладном коде, – это текущее время, и timezone.now() автоматически делает всё правильно.

По умолчанию часовой пояс и текущий часовой пояс

По умолчанию часовой пояс — часовой пояс, определённый настройкой TIME_ZONE.

Текущий часовой пояс — часовой пояс, используемый для отображения.

Вы должны установить текущий часовой пояс на фактический часовой пояс конечного пользователя с помощью activate(). В противном случае используется часовой пояс по умолчанию.

Примечание

Как объясняется в документации TIME_ZONE, Django устанавливает переменные окружения, чтобы его процесс выполнялся в часовом поясе по умолчанию. Это происходит независимо от значения USE_TZ и текущего часового пояса.

Когда USE_TZ равно True, это полезно для сохранения обратной совместимости с приложениями, которые по-прежнему полагаются на местное время. Однако, как объяснено выше, это не совсем надёжно, и вы всегда должны работать с осознанными датами и временами в UTC в собственном коде. Например, используйте fromtimestamp() и установите параметр tz на utc.

Выбор текущего часового пояса

Текущий часовой пояс соответствует текущему языковому стандарту для переводов. Однако нет эквивалента заголовку Accept-Language HTTP, который Django мог бы использовать для автоматического определения часового пояса пользователя. Вместо этого Django предоставляет функции выбора часового пояса. Используйте их для создания логики выбора часового пояса, которая имеет смысл для вас.

Большинство сайтов, которые заботятся о часовых поясах, спрашивают пользователей, в каком часовом поясе они живут, и сохраняют эту информацию в профиле пользователя. Для анонимных пользователей используется часовой пояс основной аудитории или UTC. zoneinfo.available_timezones() предоставляет набор доступных часовых поясов, которые вы можете использовать для создания карты из вероятных местоположений в часовые пояса.

Вот пример, который сохраняет текущий часовой пояс в сессии. (Обработка ошибок полностью пропущена ради простоты.)

Добавьте указанный ниже middleware в MIDDLEWARE:

import zoneinfo

from django.utils import timezone

class TimezoneMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        tzname = request.session.get('django_timezone')
        if tzname:
            timezone.activate(zoneinfo.ZoneInfo(tzname))
        else:
            timezone.deactivate()
        return self.get_response(request)

Создайте представление, которое может установить текущий часовой пояс:

from django.shortcuts import redirect, render

# Prepare a map of common locations to timezone choices you wish to offer.
common_timezones = {
    'London': 'Europe/London',
    'Paris': 'Europe/Paris',
    'New York': 'America/New_York',
}

def set_timezone(request):
    if request.method == 'POST':
        request.session['django_timezone'] = request.POST['timezone']
        return redirect('/')
    else:
        return render(request, 'template.html', {'timezones': common_timezones})

Включите форму в template.html, которая будет POST в это представление:

{% load tz %}
{% get_current_timezone as TIME_ZONE %}
<form action="{% url 'set_timezone' %}" method="POST">
    {% csrf_token %}
    <label for="timezone">Time zone:</label>
    <select name="timezone">
        {% for city, tz in timezones %}
        <option value="{{ tz }}"{% if tz == TIME_ZONE %} selected{% endif %}>{{ city }}</option>
        {% endfor %}
    </select>
    <input type="submit" value="Set">
</form>

Часовой пояс в формах

При включении поддержки часовых поясов Django интерпретирует даты и время, введённые в формах, в текущем часовом поясе и возвращает осознанные объекты datetime в cleaned_data. Конвертированные даты и времена, которые не существуют или являются неоднозначными из-за перехода на летнее время, будут отображаться как недопустимые значения.

Вывод в шаблонах с учётом часового пояса

При включении поддержки часовых поясов Django преобразует объекты datetime с учётом часового пояса в текущий часовой пояс при их отображении в шаблонах. Это поведение очень похоже на локализацию форматов.

Предупреждение

Django не преобразует объекты datetime без часового пояса, так как они могут быть неоднозначными, и потому ваш код никогда не должен создавать объекты datetime без часового пояса, когда включена поддержка часовых поясов. Однако, вы можете принудительно выполнить преобразование с помощью фильтров шаблонов, описанных ниже.

Преобразование во время местного времени не всегда уместно – вы можете генерировать вывод для компьютеров, а не для людей. Следующие фильтры и теги, предоставленные библиотекой шаблонов tz, позволяют вам управлять преобразованиями часовых поясов.

Теги шаблонов

localtime

Включает или отключает преобразование объектов datetime с учётом часового пояса в текущий часовой пояс в содержащемся блоке.

Этот тег имеет точно такое же воздействие, как и настройка USE_TZ, что касается движка шаблонов. Он позволяет более тонко управлять преобразованиями.

Чтобы активировать или деактивировать преобразование для блока шаблона, используйте:

{% load tz %}

{% localtime on %}
    {{ value }}
{% endlocaltime %}

{% localtime off %}
    {{ value }}
{% endlocaltime %}

Примечание

Значение USE_TZ не учитывается внутри блока {% localtime %}.

timezone

Устанавливает или сбрасывает текущий часовой пояс в содержащемся блоке. При сбросе текущего часового пояса применяется значение по умолчанию.

{% load tz %}

{% timezone "Europe/Paris" %}
    Paris time: {{ value }}
{% endtimezone %}

{% timezone None %}
    Server time: {{ value }}
{% endtimezone %}

get_current_timezone

Вы можете получить имя текущего часового пояса с помощью тега get_current_timezone:

{% get_current_timezone as TIME_ZONE %}

В качестве альтернативы, вы можете активировать обработчик контекста tz() и использовать переменную контекста TIME_ZONE.

Фильтры шаблонов

Эти фильтры принимают как объекты datetime с учётом часового пояса, так и без него. Для целей преобразования они предполагают, что объекты datetime без часового пояса находятся в часовом поясе по умолчанию. Они всегда возвращают объекты datetime с учётом часового пояса.

localtime

Принудительно преобразует отдельное значение в текущий часовой пояс.

Например:

{% load tz %}

{{ value|localtime }}

utc

Принудительно преобразует отдельное значение в UTC.

Например:

{% load tz %}

{{ value|utc }}

timezone

Принудительно преобразует отдельное значение в произвольный часовой пояс.

Аргумент должен быть экземпляром подкласса tzinfo или именем часового пояса.

Например:

{% load tz %}

{{ value|timezone:"Europe/Paris" }}

Руководство по миграции часовых поясов

Вот как мигрировать проект, который был создан до того, как Django поддерживал часовые пояса.

База данных

PostgreSQL

Бэкенд PostgreSQL хранит объекты datetime в формате timestamp with time zone. На практике это означает, что он преобразует объекты datetime из часового пояса подключения в UTC при сохранении и из UTC в часовой пояс подключения при получении.

Следовательно, если вы используете PostgreSQL, вы можете свободно переключаться между USE_TZ = False и USE_TZ = True. Часовой пояс подключения к базе данных будет установлен на TIME_ZONE или UTC соответственно, чтобы Django получал правильные объекты datetime во всех случаях. Вам не нужно выполнять никаких преобразований данных.

Другие базы данных

Другие бэкенды хранят объекты datetime без информации о часовом поясе. Если вы переключаетесь с USE_TZ = False на USE_TZ = True, вы должны преобразовать данные из местного времени в UTC – что не является детерминированным, если местное время имеет летнее время.

Код

Первый шаг – добавить USE_TZ = True в файл настроек. На этом этапе всё должно работать в основном. Если вы создаёте объекты datetime без часового пояса в своём коде, Django делает их осознанными при необходимости.

Однако, эти преобразования могут не выполняться корректно вокруг переходов на летнее/зимнее время, а это значит, что вы ещё не получаете все преимущества поддержки часовых поясов. Кроме того, вы, вероятно, столкнётесь с несколькими проблемами, потому что невозможно сравнить объект datetime без часового пояса с объектом datetime с учётом часового пояса. Поскольку Django теперь предоставляет объекты datetime с учётом часового пояса, вы получите исключения всякий раз, когда сравниваете объект datetime, который получен из модели или формы, с объектом datetime без часового пояса, который вы создали в своём коде.

Поэтому второй шаг – переработать ваш код, где вы создаёте объекты datetime, чтобы сделать их осознанными. Это можно сделать поэтапно. django.utils.timezone определяет некоторые полезные вспомогательные функции для кода совместимости: now(), is_aware(), is_naive(), make_aware() и make_naive().

Наконец, для того, чтобы помочь вам найти код, который требует обновления, Django выводит предупреждение, когда вы пытаетесь сохранить объект datetime без часового пояса в базе данных:

RuntimeWarning: DateTimeField ModelName.field_name received a naive
datetime (2012-01-01 00:00:00) while time zone support is active.

Во время разработки вы можете превратить такие предупреждения в исключения и получить traceback, добавив следующее в файл настроек:

import warnings
warnings.filterwarnings(
    'error', r"DateTimeField .* received a naive datetime",
    RuntimeWarning, r'django\.db\.models\.fields',
)

Файлы настроек

При сериализации объекта datetime с учётом часового пояса смещение UTC включается, как в этом примере:

"2011-09-01T13:20:30+03:00"

В то время как для объекта datetime без часового пояса этого нет:

"2011-09-01T13:20:30"

Для моделей с полями DateTimeField это различие делает невозможным создание файла настроек, который работает как с поддержкой часовых поясов, так и без неё.

Файлы настроек, созданные с помощью USE_TZ = False, или до Django 1.4, используют формат «без часового пояса». Если ваш проект содержит такие файлы настроек, после включения поддержки часовых поясов вы увидите предупреждения RuntimeWarning при их загрузке. Чтобы избавиться от предупреждений, вы должны преобразовать ваши файлы настроек в формат «с часовым поясом».

Вы можете перегенерировать файлы настроек с помощью loaddata и затем dumpdata. Или, если они достаточно небольшие, вы можете отредактировать их, чтобы добавить смещение UTC, соответствующее вашему TIME_ZONE, к каждому сериализованному объекту datetime.

Вопросы и ответы по часовым поясам

Настройка

  1. Мне не нужны несколько часовых поясов. Нужно ли включать поддержку часовых поясов?

    Да. Когда включена поддержка часовых поясов, Django использует более точную модель местного времени. Это защищает вас от тонких и невоспроизводимых ошибок при переходах на летнее/зимнее время (DST).

    Когда вы включаете поддержку часовых поясов, вы столкнетесь с некоторыми ошибками, потому что используете объекты datetime без часового пояса, где Django ожидает объекты datetime с учётом часового пояса. Такие ошибки появляются при запуске тестов. Вы быстро научитесь избегать некорректных операций.

    С другой стороны, ошибки, вызванные отсутствием поддержки часовых поясов, гораздо труднее предотвратить, диагностировать и исправить. Всё, что связано с запланированными задачами или арифметикой с датами и временем, является кандидатом для тонких ошибок, которые будут беспокоить вас только один-два раза в год.

    По этим причинам поддержка часовых поясов включена по умолчанию в новых проектах, и вы должны её сохранять, если у вас нет веской причины не делать этого.

  2. Я включил поддержку часовых поясов. Я в безопасности?

    Возможно. Вы защищены от ошибок, связанных с переходами на летнее/зимнее время, но вы всё ещё можете навредить себе, небрежно превращая объекты datetime без часового пояса в объекты datetime с учётом часового пояса и наоборот.

    Если ваше приложение подключается к другим системам – например, если оно обращается к веб-сервису – убедитесь, что даты и время правильно указаны. Для безопасной передачи дат и времени их представление должно включать смещение UTC или их значения должны быть в UTC (или и то, и другое!).

    Наконец, наша система календаря содержит интересные крайние случаи. Например, вы не всегда можете напрямую вычесть один год из заданной даты:

    >>> import datetime
    >>> def one_year_before(value):  # Wrong example.
    ...     return value.replace(year=value.year - 1)
    >>> one_year_before(datetime.datetime(2012, 3, 1, 10, 0))
    datetime.datetime(2011, 3, 1, 10, 0)
    >>> one_year_before(datetime.datetime(2012, 2, 29, 10, 0))
    Traceback (most recent call last):
    ...
    ValueError: day is out of range for month
    

    Чтобы правильно реализовать такую функцию, вы должны решить, является ли 2012-02-29 минус один год 2011-02-28 или 2011-03-01, что зависит от ваших бизнес-требований.

  3. Как взаимодействовать с базой данных, которая хранит даты и время в местном времени?

    Установите опцию TIME_ZONE на соответствующий часовой пояс для этой базы данных в настройке DATABASES.

    Это полезно для подключения к базе данных, которая не поддерживает часовые пояса и не управляется Django, когда USE_TZ включён.

Устранение неполадок

  1. Моё приложение падает с TypeError: can't compare offset-naive and offset-aware datetimes – в чём проблема?

    Давайте воспроизведём эту ошибку, сравнив наивную и осознанную дату и время:

    >>> from django.utils import timezone
    >>> aware = timezone.now()
    >>> naive = timezone.make_naive(aware)
    >>> naive == aware
    Traceback (most recent call last):
    ...
    TypeError: can't compare offset-naive and offset-aware datetimes
    

    Если вы столкнулись с этой ошибкой, скорее всего, ваш код сравнивает эти два объекта:

    • дата и время, предоставленная Django – например, значение, считанное из формы или поля модели. Поскольку вы включили поддержку часовых поясов, она является осознанной.
    • дата и время, сгенерированная вашим кодом, которая является наивной (иначе вы бы этого не читали).

    В общем случае правильное решение состоит в том, чтобы изменить ваш код на использование осознанной даты и времени.

    Если вы пишете подключаемый модуль, который должен работать независимо от значения USE_TZ, вы можете найти полезной функцию django.utils.timezone.now(). Эта функция возвращает текущую дату и время как наивную дату и время, когда USE_TZ = False и как осознанную дату и время, когда USE_TZ = True. Вы можете добавлять или вычитать datetime.timedelta по мере необходимости.

  2. Я вижу много RuntimeWarning: DateTimeField received a naive datetime (YYYY-MM-DD HH:MM:SS) while time zone support is active – это плохо?

    Когда включена поддержка часовых поясов, слой базы данных ожидает получить только осознанные даты и время от вашего кода. Это предупреждение возникает, когда он получает наивную дату и время. Это указывает на то, что вы не завершили портирование своего кода для поддержки часовых поясов. Обратитесь к руководству по миграции за советами по этому процессу.

    Тем временем, для обратной совместимости, дата и время рассматривается как находящаяся в стандартном часовом поясе, что обычно и ожидается.

  3. now.date() вчера! (или завтра)

    Если вы всегда использовали наивные даты и время, вы, вероятно, считаете, что можете преобразовать дату и время в дату, вызвав метод date(). Вы также считаете, что date очень похожа на datetime, за исключением того, что она менее точная.

    Ничего из этого неверно в среде с осознанием часового пояса:

    >>> import datetime
    >>> import zoneinfo
    >>> paris_tz = zoneinfo.ZoneInfo("Europe/Paris")
    >>> new_york_tz = zoneinfo.ZoneInfo("America/New_York")
    >>> paris = datetime.datetime(2012, 3, 3, 1, 30, tzinfo=paris_tz)
    # This is the correct way to convert between time zones.
    >>> new_york = paris.astimezone(new_york_tz)
    >>> paris == new_york, paris.date() == new_york.date()
    (True, False)
    >>> paris - new_york, paris.date() - new_york.date()
    (datetime.timedelta(0), datetime.timedelta(1))
    >>> paris
    datetime.datetime(2012, 3, 3, 1, 30, tzinfo=zoneinfo.ZoneInfo(key='Europe/Paris'))
    >>> new_york
    datetime.datetime(2012, 3, 2, 19, 30, tzinfo=zoneinfo.ZoneInfo(key='America/New_York'))
    

    Как показывает этот пример, у одной и той же даты и времени разная дата в зависимости от часового пояса, в котором она представлена. Но реальная проблема более фундаментальна.

    Дата и время представляют собой точку во времени. Она абсолютна: не зависит ни от чего. Напротив, дата – это понятие календаря. Это период времени, границы которого зависят от часового пояса, в котором рассматривается дата. Как видите, эти два понятия принципиально разные, а преобразование даты и времени в дату не является детерминированной операцией.

    Что это значит на практике?

    В целом следует избегать преобразования datetime в date. Например, вы можете использовать фильтр шаблонов date, чтобы отобразить только часть даты даты и времени. Этот фильтр преобразует дату и время в текущий часовой пояс перед форматированием, гарантируя правильное отображение результатов.

    Если вам действительно нужно выполнить преобразование самостоятельно, вы должны сначала убедиться, что дата и время преобразованы в соответствующий часовой пояс. Обычно это будет текущий часовой пояс:

    >>> from django.utils import timezone
    >>> timezone.activate(zoneinfo.ZoneInfo("Asia/Singapore"))
    # For this example, we set the time zone to Singapore, but here's how
    # you would obtain the current time zone in the general case.
    >>> current_tz = timezone.get_current_timezone()
    >>> local = paris.astimezone(current_tz)
    >>> local
    datetime.datetime(2012, 3, 3, 8, 30, tzinfo=zoneinfo.ZoneInfo(key='Asia/Singapore'))
    >>> local.date()
    datetime.date(2012, 3, 3)
    
  4. Я получаю ошибку “Are time zone definitions for your database installed?”

    Если вы используете MySQL, см. раздел Определения часовых поясов в примечаниях MySQL для получения инструкций по загрузке определений часовых поясов.

Использование

  1. У меня есть строка "2012-02-21 10:28:45" и я знаю, что она в "Europe/Helsinki" часовом поясе. Как я могу преобразовать её в осознанную дату и время?

    Здесь вам нужно создать необходимый ZoneInfo объект и прикрепить его к наивной дате и времени:

    >>> import zoneinfo
    >>> from django.utils.dateparse import parse_datetime
    >>> naive = parse_datetime("2012-02-21 10:28:45")
    >>> naive.replace(tzinfo=zoneinfo.ZoneInfo("Europe/Helsinki"))
    datetime.datetime(2012, 2, 21, 10, 28, 45, tzinfo=zoneinfo.ZoneInfo(key='Europe/Helsinki'))
    
  2. Как я могу получить локальное время в текущем часовом поясе?

    Ну, первый вопрос: вам это действительно нужно?

    Вы должны использовать локальное время только при взаимодействии с людьми, и слой шаблонов предоставляет фильтры и теги для преобразования дат и времени в часовой пояс по вашему выбору.

    Кроме того, Python умеет сравнивать осознанные даты и время, учитывая смещения UTC при необходимости. Гораздо проще (и, возможно, быстрее) написать весь ваш код модели и представления в UTC. Таким образом, в большинстве случаев даты и время в UTC, возвращаемые django.utils.timezone.now(), будут достаточны.

    Однако, для полноты картины, если вам действительно нужно локальное время в текущем часовом поясе, вот как его можно получить:

    >>> from django.utils import timezone
    >>> timezone.localtime(timezone.now())
    datetime.datetime(2012, 3, 3, 20, 10, 53, 873365, tzinfo=zoneinfo.ZoneInfo(key='Europe/Paris'))
    

    В этом примере текущий часовой пояс – "Europe/Paris".

  3. Как я могу увидеть все доступные часовые поясы?

    zoneinfo.available_timezones() предоставляет набор всех допустимых ключей для часовых поясов IANA, доступных для вашей системы. См. документацию для использования.

© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/4.1/topics/i18n/timezones/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API