Django-rest-framework: Сериализатор DateTimeField имеет неожиданную информацию о часовом поясе

Созданный на 13 дек. 2015  ·  34Комментарии  ·  Источник: encode/django-rest-framework

У меня в настройках есть TIME_ZONE = 'Asia/Kolkata' и USE_TZ = True .

Когда я создаю новый объект с доступным для просмотра api, сериализатор отображает только что созданный объект с датами, которые имеют завершающий +5:30 , указывающий часовой пояс. База данных хранит время в формате UTC.

Неожиданное поведение заключается в том, что, когда объект снова сериализуется позже, все даты и время находятся в формате UTC с завершающим Z . Согласно документам Django о текущем часовом поясе и часовом поясе по умолчанию, я ожидаю, что сериализатор преобразует дату и время в текущий часовой пояс, который по умолчанию соответствует часовому поясу по умолчанию, который установлен TIME_ZONE = 'Asia/Kolkata' .

Я что-то упускаю?

Needs further review

Самый полезный комментарий

Также был бы признателен за эту функцию. Я считаю, что следует соблюдать настройку USE_TZ и преобразовывать значения аналогично стандартному поведению Django.

Все 34 Комментарий

Я спросил о переполнении стека, и пришел к выводу, что текущий часовой пояс используется только для заданных пользователем значений даты и времени, поскольку дата и время сериализуются как есть после проверки (т.е. после создания или обновления). Из документации Django мне трудно поверить, что это предполагаемое поведение, но это проблема самого Django, а не Rest Framework, поэтому я закрою эту проблему.

Привет @tomchristie!

Думаю, это ошибка DRF.
IMHO, естественное использование DRF - использовать его в качестве вывода, как шаблон django.
В шаблоне django часовой пояс отображается правильно, почему сериализатор drf не соблюдает настройку часового пояса django?

Привет,

просматривая код в fields.py в DatetimeField class, я обнаруживаю, что настройка часового пояса Django хорошо учитывается только для внутреннего значения ( to_internal_value() ).

Я видел это с помощью сериализатора моделей, подобного этому:

class MyModelSerializer(ModelSerializer):

    class Meta:
        model = MyModel
        depth = 1
        fields = ('some_field', 'my_date_time_field')

для поля my_date_time_field (которое, я думаю, это DateTimeField :)) часовой пояс для представления по умолчанию равен None ( to_representation() ).

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

IMHO Я думаю, что возвращаемое значение to_representation() должно быть таким:
return self.enforce_timezone(value).strftime(output_format)
согласно настройке Django USE_TZ .

Я пишу для этого пул-реквест.

Чао
Валентино

@vpistis Было бы легче просмотреть это, если бы вы могли привести пример поведения API, которое вы сейчас видите, и того, что вы ожидаете увидеть (вместо того, чтобы сначала обсуждать аспекты реализации)

Установите TIME_ZONE = 'Asia/Kolkata' и создайте сериализатор модели с одним полем datetime, appointment .

Во время создания / обновления отправьте:

{
    "appointment": "2016-12-19T10:00:00"
}

и вернись:

{
    "appointment": "2016-12-19T10:00:00+5:30"
}

Но если вы снова получите этот объект или внесете его в список, вы получите:

{
    "appointment": "2016-12-19T04:30:00Z"
}

Во время создания, если Z не указан, DRF предполагает, что клиент использует часовой пояс, указанный в TIME_ZONE и возвращает время, отформатированное в соответствии с этим часовым поясом (путем добавления +5:30 в конце, что фактически было бы недействительным, если бы это было время, указанное клиентом). Вероятно, было бы больше смысла, если бы будущие обращения также возвращали время, отформатированное в этом часовом поясе, поэтому ответ во время извлечения / списка был таким же, как и во время создания / обновления.

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

{
    "appointment": "2016-12-19T04:30:00Z"
}

возвращает:

{
    "appointment": "2016-12-19T10:00:00+5:30"
}

Я был бы за это, поскольку он поддерживает согласованность ответа с ответом на списки / поиски.

Другой вариант - всегда возвращать время UTC, даже во время создания / обновления, но я считаю это менее полезным. В любом случае, постоянные часовые пояса были бы предпочтительнее ситуации 50/50, которая есть у нас сейчас.

Установите TIME_ZONE = 'Asia / Kolkata' и создайте сериализатор модели с одним полем datetime, assign.

Во время создания / обновления отправьте:

{
«назначение»: «2016-12-19T10: 00: 00»
}
и вернись:

{
«назначение»: «2016-12-19T10: 00: 00 + 5: 30»
}
Но если вы снова получите этот объект или внесете его в список, вы получите:

{
«назначение»: «2016-12-19T04: 30: 00Z»
}

thanx @ jonathan-golorry, это именно то поведение, которое я на самом деле наблюдаю.
Для меня поведение должно быть таким (на примере @ jonathan-golorry :)):

Во время создания / обновления с DATETIME_FORMAT по умолчанию отправьте:

{
    "appointment": "2016-12-19T10:00:00"
}

и вернись:

{
    "appointment": "2016-12-19T10:00:00+5:30"
}

Если вы снова извлечете или внесете в список этот объект, вы получите:

{
    "appointment": "2016-12-19T10:00:00+5:30"
}

IMHO, возможно, должен быть параметр DRF для управления этим поведением, например, параметр, заставляющий DateTimeField быть представлен с часовым поясом по умолчанию.

большое спасибо @tomchristie

Несоответствие между различными действиями является решающим аргументом для повторного открытия здесь. Я не понимал, что это было так. Я бы ожидать , что мы использовать UTC в течение по умолчанию, хотя мы могли бы сделать это необязательным.

Для производственного веб-приложения, использующего DRF 3.4.6, мы решили следующее обходное решение:
https://github.com/vpistis/django-rest-framework/commit/be62db9080b19998d4de3a1f651a291d691718f6

Если кто-то хочет отправить запрос на перенос, который включает:

  • просто включает в себя неудачные тестовые примеры или ...
  • тестовый пример + исправление

это было бы очень приятно.

Я написал код для тестирования, но я не уверен, как использовать тестовые примеры drf. Я не знаю, как управлять настройками django, чтобы изменять часовой пояс и другие настройки во время выполнения.
Пожалуйста, дайте мне ссылку на конкретный пример или руководство.

спасибо

Если вы хотите изменить глобальные настройки теста, они находятся здесь .

Если вы пытаетесь изменить настройки во время тестирования, вы можете использовать декоратор override_settings .

Также был бы признателен за эту функцию. Я считаю, что следует соблюдать настройку USE_TZ и преобразовывать значения аналогично стандартному поведению Django.

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

Привет,
для меня это тестовый пример для этой функции

class TestDateTimeFieldTimeZone(TestCase):
    """
    Valid and invalid values for `DateTimeField`.
    """
    from django.utils import timezone

    valid_inputs = {
        '2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.get_default_timezone()),
        '2001-01-01T13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.get_default_timezone()),
        '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.get_default_timezone()),
        datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.get_default_timezone()),
        datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00,
                                                                                        tzinfo=timezone.get_default_timezone()),
        # Django 1.4 does not support timezone string parsing.
        '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.get_default_timezone())
    }
    invalid_inputs = {}
    outputs = {
        # This is not simple, for now I suppose TIME_ZONE = "Europe/Rome"
        datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.get_default_timezone()): '2001-01-01T13:00:00+01:00',
        datetime.datetime(2001, 1, 1, 13, 00, ): '2001-01-01T13:00:00+01:00',
    }

    field = serializers.DateTimeField()

В своей вилке я использую какой-то трюк, чтобы получить время в правильном часовом поясе, мой 3.6.2_tz_fix .

Надеюсь, это поможет :)

Я вижу, что это закрыто, но я запускаю drf 3.6.3 и в моей базе данных postgres у меня есть эта отметка времени «2017-07-12 14: 26: 00-06», но когда я получаю данные с помощью почтальона, я получаю эту «отметку времени» : "2017-07-12T20: 26: 00Z". Похоже, он прибавляет -06 часов.

В моих настройках django используется tzlocal для установки часового пояса TIME_ZONE = str(get_localzone()) . Таким образом, часовой пояс устанавливается при запуске.

Я использую базовую модель сериализатора

class SnapshotSerializer(serializers.ModelSerializer):
    class Meta:
        model = Snapshot
        resource_name = 'snapshot' 
        read_only_fields = ('id',)
        fields = ('id', 'timestamp', 'snapshot')

Я что-то упускаю?

не закрыт, он еще открыт :)
красная «закрытая кнопка», которую вы видите, предназначена для справки.
... и вы правы, "баг" все еще есть; (
Эта веха изменена с 3.6.3 на 3.6.4.

Ох, хорошо. Спасибо!

12 июля 2017 г., 17:10, "Валентино Пистис" [email protected]
написал:

не закрыт, он еще открыт :)
красная «закрытая кнопка», которую вы видите, предназначена для справки.
... и вы правы, "баг" все еще есть; (
Эта веха изменена с 3.6.3 на 3.6.4.

-
Вы получили это, потому что прокомментировали.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/encode/django-rest-framework/issues/3732#issuecomment-314923582 ,
или отключить поток
https://github.com/notifications/unsubscribe-auth/AIMBTcx-6PPbi_SOqeLCjeWV1Rb59-Ohks5sNVJ0gaJpZM4G0aRE
.

Привет,

Я не уверен, что это полностью связано с исходным вопросом, но должен ли DRF учитывать любые установленные переопределения часового пояса в соответствии с https://docs.djangoproject.com/en/1.11/ref/utils/#django. utils.timezone.activate ?

У меня есть система, в которой мои пользователи связаны с часовым поясом, а API получает наивные даты. Я ожидаю, что смогу преобразовать эти даты в часовой пояс текущего пользователя, но я заметил, что ../rest_framework/fields.py применяет часовой пояс по умолчанию (то есть тот, который указан в файле настроек django:

    def enforce_timezone(self, value):
        field_timezone = getattr(self, 'timezone', self.default_timezone())

        if (field_timezone is not None) and not timezone.is_aware(value):
            try:
                return timezone.make_aware(value, field_timezone)

[...]

    def default_timezone(self):
        return timezone.get_default_timezone() if settings.USE_TZ else None

Должно ли это действительно использоваться timezone.get_current_timezone() в качестве предпочтения в случае, если приложение установило переопределение, например, в моем случае?

Привет, @RichardForshaw, это похоже на отдельную проблему, но, безусловно, тот же парк мячей.

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

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

Но да, здесь есть одно несоответствие. (Я упоминал "Добро пожаловать в PR?")

Здесь должен быть актуален carltongibson / django-filter # 750. Изначально я основывал обработку часовых поясов django-filter на DRF, поэтому здесь можно легко применить изменения в 750.

Извините за то, что я новичок, но в чем именно проблема? Отметка времени в моей базе данных psql верна, и Django настроен на использование правильного часового пояса. Есть ли настройки DRF, чтобы он не конвертировал временные метки?

Привет @michaelaelise - если вы посмотрите на пример данных (вверху):

  1. Они отправляют дату и время без информации о часовом поясе. (Это плохой ход в моей книге.)
  2. Сервер применяет свой местный часовой пояс, и он возвращается как это ( +5:30 в этом случае)
  3. Но позже, когда вы его загрузите, он возвращается как UTC ( Z , я полагаю, для "Zulu").

Нет никакой реальной проблемы в предположении, что ваш клиент будет обрабатывать преобразование часового пояса за вас . (Кроме, может быть, №1, потому что кто сказал, что у вашего клиента тот же часовой пояс, что и у вашего сервера ...?)

Но это небольшая несогласованность, неужели 2 и 3 должны иметь одинаковый формат? (Даже если клиент правильно примет любое из них как эквивалентные значения.)

Я склонен закрыть это.

  • Здесь нет логической ошибки.

    • Это одно и то же время: "2016-12-19T10:00:00+5:30" и "2016-12-19T04:30:00Z" - в какой-то степени кого волнует, как они вернутся?

  • Таким образом, это не то, на что я могу действительно тратить время.
  • Билету 2 года и пиара никто не предлагал.

Я счастлив посмотреть на PR, но я не уверен, что действительно хочу рассматривать это как _Open Issue_.

О, я не понимал, что в моем исходном сообщении в этой ветке выпуска "Z" означало именно это.
Итак, DRF преобразует известную дату и время в UTC для обработки пользовательским интерфейсом / вызывающей стороной?
Спасибо за это разъяснение.

Последняя вещь.
Что, если мы хотим, чтобы возвращалось «2016-12-19T10: 00: 00 + 5: 30», потому что мы опрашиваем устройства в разных часовых поясах.
Может быть, это настройка «RETURN_DATETIME_WITH_TIMEZONE»?

Мы используем django / drf на периферийных устройствах. Таким образом, все вставленные даты и времени не заботятся о том, наивно это или нет, потому что часовой пояс граничного устройства настроен, а поле postgres всегда будет точным для даты и времени этого устройства.

Облачный сервер в текущем сценарии затем должен будет знать часовой пояс каждого устройства, вероятно, так оно и будет, что просто переключает работу с django / drf на облачное приложение для обработки.

Предполагая, что USE_TZ, DRF уже возвращает дату и время с информацией о часовом поясе. Так что он уже делает то, что вам нужно.

Единственная проблема здесь заключается в том, отформатировано ли одно и то же DT для того или иного часового пояса. (Но они все еще в то же время.)

@carltongibson

Здесь нет логической ошибки.
Это одно и то же время: «2016-12-19T10: 00: 00 + 5: 30» и «2016-12-19T04: 30: 00Z» - в какой-то степени кого волнует, как они вернутся?

ИМХО в этом проблема: возвращенная строка!
Я использую настройки часового пояса Django, и все шаблоны возвращают правильное время "2016-12-19T10:00:00+5:30" как мы и ожидали, но DRF - нет. DRF возвращает "2016-12-19T04:30:00Z" .
В клиент, который потребляет мой API REST, нет никакой логики, нет преобразований времени или интерпретации строки даты и времени.
Другими словами, я ожидаю, что дата и время из ответа DRF идентичны «ответу» шаблона Django: сервер подготавливает все данные для клиента, а клиент только показывает их.

В любом случае, большое спасибо за ваше терпение, поддержку и отличную работу над этим фантастическим проектом!

@vpistis, я

Я ожидаю, что пользователи будут анализировать строку даты на Date, однако их клиентский язык предусматривает это, а не потребляет необработанную строку.

Я согласен, если вы потребляете необработанную строку, ваши ожидания здесь не оправдаются. (Но не делайте этого: представьте, если бы мы отправили временные метки UNIX; вы ни за что не стали бы использовать их в сыром виде. Преобразуйте в правильный объект Date, независимо от того, что это на вашем клиентском языке.)

Я действительно счастлив получить пиар по этому поводу. (Еще не закрыл!)

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

(Поэтому я склонен закрыть его на том основании, что мы возьмем пиар, если он когда-нибудь появится)

Привет всем, это должно быть исправлено с помощью # 5408. Если у вас есть время установить ветку и убедиться, что все работает должным образом, это было бы фантастически. Спасибо!

Я думаю, что проблема была как-то повторно введена:

Когда я изменил TZ по умолчанию с UTC на Europe / Amsterdam, один из тестов не прошел, и я заметил, что DRF сериализуется во что-то отличное от TZ по умолчанию.

изменить: проблема была связана с тестовой / заводской настройкой.


Схема тестирования ниже.

модель

class Something(StampedModelMixin):
    MIN_VALUE = 1
    MAX_VALUE = 500

    id = models.BigAutoField(primary_key=True)  # pylint: disable=blacklisted-name
    product_id = models.BigIntegerField(db_index=True)
    start_time = models.DateTimeField()
    end_time = models.DateTimeField()
    percentage = models.IntegerField()
    enabled = models.IntegerField()

фабрика

class SomethingFactory(factory.django.DjangoModelFactory):
    """ Base Factory to create records for Something

    """
    start_time = factory.Faker('date_time', tzinfo=get_default_timezone())
    end_time = factory.Faker('date_time', tzinfo=get_default_timezone())
    percentage = factory.Faker('random_int', min=1, max=500)
    enabled = factory.Faker('random_element', elements=[0, 1])

    class Meta:  # pylint: disable=missing-docstring
        model = Something

модульный тест

class TestSomething:
    def test__get__empty(self):
        # preparation of data
        series = SeriesFactory.create(product_id=2)
        SomethingFactory.create_batch(3, product_id=1)

        # prepare request params
        url = reverse('series-somethings', kwargs={'pk': series.pk})

        # call the endpoint
        response = self.client.get(url)

        # asserts
        assert response.data == []

    def test__get__single(self):
        # preparation of data
        series = SeriesFactory.create(product_id=1)
        old_somethings = SomethingFactory.create_batch(1, product_id=1)

        # prepare request params
        url = reverse('series-somethings', kwargs={'pk': series.pk})

        # call the endpoint
        response = self.client.get(url)

        # asserts
        assert SomethingSerializer(old_somethings, many=True).data == response.data

Посмотреть

class SomethingElseView(APILogMixin, ModelViewSet):
    @action(detail=True, methods=['get'])
    def somethings(self, request, pk=None):
        """ GET endpoint for Somethings

        .. seealso:: :func:`rest_framework.decorators.action`
        """
        otherthings = self.get_object()
        something_qs = Something.objects.all()
        something_qs = something_qs.filter(product_id=otherthings.product_id)
        serializer = self.something_serializer_class(something_qs, many=True)
        return Response(serializer.data)

Сериализатор

class SomethingSerializer(serializers.ModelSerializer):

    class Meta:
        model = Something
        list_serializer_class = SomethingListSerializer
        fields = '__all__'
        extra_kwargs = {
            'percentage': {'min_value': Something.MIN_VALUE,
                           'max_value': Something.MAX_VALUE}
        }
        read_only_fields = ('id',
                            'ts_activated',
                            'ts_created',
                            'ts_updated')

Результат теста ipdb

ipdb> old_somethings
[{'product_id': 1, 'start_time': datetime.datetime(2011, 7, 13, 1, 10, 33, tzinfo=<DstTzInfo 'Europe/Amsterdam' LMT+0:20:00 STD>), 'end_time': datetime.datetime(2003, 3, 10, 9, 31, tzinfo=<DstTzInfo 'Europe/Amsterdam' LMT+0:20:00 STD>), 'percentage': 103, 'enabled': 0}]
ipdb> response.data
[OrderedDict([('id', 1), ('ts_created', '2019-02-27 14:16:33'), ('ts_updated', '2019-02-27 14:16:33'), ('product_id', 1), ('start_time', '2011-07-13 02:50:33'), ('end_time', '2003-03-10 10:11:00'), ('percentage', 103), ('enabled', 0)])]

Результат испытаний

E       AssertionError: assert [OrderedDict(...nabled', 0)])] == [OrderedDict([...nabled', 0)])]
E         At index 0 diff: OrderedDict([('id', 1), ('ts_created', '2019-02-27 14:38:15'), ('ts_updated', '2019-02-27 14:38:15'), ('product_id', 1), ('start_time', '2011-07-13 01:10:33'), ('end_time', '2003-03-10 09:31:00'), ('percentage', 103), ('enabled', 0)]) != OrderedDict([('id', 1), ('ts_created', '2019-02-27 14:38:15'), ('ts_updated', '2019-02-27 14:38:15'), ('product_id', 1), ('start_time', '2011-07-13 02:50:33'), ('end_time', '2003-03-10 10:11:00'), ('percentage', 103), ('enabled', 0)])
E         Full diff:
E         - [OrderedDict([('id', 1), ('ts_created', '2019-02-27 14:38:15'), ('ts_updated', '2019-02-27 14:38:15'), ('product_id', 1), ('start_time', '2011-07-13 01:10:33'), ('end_time', '2003-03-10 09:31:00'), ('percentage', 103), ('enabled', 0)])]
E         ?                                                                                                                                                       ^^^^^^^^^^                          ^^^^^^^^^^^
E         + [OrderedDict([('id', 1), ('ts_created', '2019-02-27 14:38:15'), ('ts_updated', '2019-02-27 14:38:15'), ('product_id', 1), ('start_time', '2011-07-13 02:50:33'), ('end_time', '2003-03-10 10:11:00'), ('percentage', 103), ('enabled', 0)])]
E         ?

Куча:

Django==2.0.10
djangorestframework==3.9.0
factory-boy==2.11.1
Faker==0.8.18
pytz==2018.9

Привет @ diegueus9 - не могли бы вы свести это к более простому тесту? Вы сравниваете сериализованные поддельные данные с данными ответа представления. Так что неясно, каково фактическое ожидаемое значение. Я бы рекомендовал сравнить какой-то результат с жестко запрограммированным значением. например,

assert SomethingSerializer(old_somethings, many=True).data == {'blah':'blah'}

@rpkilby спасибо за ответ. Это была моя ошибка, проблема с моими модульными тестами в том, что я использовал для него factory-boy / faker без обновления из БД, поэтому разница, я только что добавил

    for old_something in old_somethings:
        old_something.refresh_from_db()

Следует ли мне удалить свой предыдущий комментарий или оставить его на случай, если у кого-то еще будет такое же ложное срабатывание?

Привет, @ diegueus9 , не беспокойся - я просто спрятал код в теге details .

Была ли эта страница полезной?
0 / 5 - 0 рейтинги