Django-rest-framework: El serializador DateTimeField tiene información de zona horaria inesperada

Creado en 13 dic. 2015  ·  34Comentarios  ·  Fuente: encode/django-rest-framework

Tengo TIME_ZONE = 'Asia/Kolkata' y USE_TZ = True en mi configuración.

Cuando creo un nuevo objeto con la API navegable, el serializador muestra el objeto recién creado con fechas y horas que tienen un +5:30 final, lo que indica la zona horaria. La base de datos almacena las horas en UTC.

El comportamiento inesperado es que cuando el objeto se serializa nuevamente más tarde, las fechas y horas están todas en formato UTC con un Z final. De acuerdo con los documentos de Django sobre la zona horaria actual y predeterminada, esperaría que el serializador convierta la fecha y hora a la zona horaria actual, que por defecto es la zona horaria predeterminada, que se establece en TIME_ZONE = 'Asia/Kolkata' .

¿Me estoy perdiendo de algo?

Needs further review

Comentario más útil

También agradecería esta característica. Siento que la configuración USE_TZ debe respetarse y los valores deben convertirse, de manera similar al comportamiento estándar de Django.

Todos 34 comentarios

Pregunté en el desbordamiento de la pila y la conclusión es que la zona horaria actual solo se usa para las fechas y horas proporcionadas por el usuario, ya que la fecha y hora se serializa tal como está después de la validación (es decir, después de crear o actualizar). De la documentación de Django, me cuesta creer que este sea el comportamiento previsto, pero este es un problema con Django en sí y no con Rest Framework, así que cerraré este problema.

Hola @tomchristie ,

Creo que esto es un error de DRF.
En mi humilde opinión, el uso natural de DRF es usarlo como una salida como una plantilla de django.
En una plantilla de django, la zona horaria se muestra correctamente, ¿por qué el serializador drf no respeta la configuración de la zona horaria de django?

Hola,

mirando el código en fields.py en DatetimeField class descubro que la configuración de la zona horaria de Django está bien considerada solo para el valor interno ( to_internal_value() ).

He visto que usando un serializador de modelos como este:

class MyModelSerializer(ModelSerializer):

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

para el campo my_date_time_field (que creo que es un DateTimeField :)) la zona horaria de la representación es Ninguna por defecto ( to_representation() ).

En otras palabras, si no me equivoco, la zona horaria de Django se considera solo cuando el valor se escribe en el backend de almacenamiento.

En mi humilde opinión, creo que el valor de retorno de to_representation() debería ser así:
return self.enforce_timezone(value).strftime(output_format)
de acuerdo con la configuración de Django USE_TZ .

Escribo una solicitud de extracción para esto.

Ciao
Valentino

@vpistis Sería más fácil revisar esto si pudiera dar un ejemplo del comportamiento de la API que ve actualmente y lo que esperaría ver (en lugar de discutir primero los aspectos de implementación)

Configure su TIME_ZONE = 'Asia/Kolkata' y cree un serializador de modelo con un solo campo de fecha y hora, appointment .

Durante la creación / actualización, envíe:

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

y vuelve:

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

Pero si recupera o enumera ese objeto nuevamente, obtiene:

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

Durante la creación, si no se especifica la Z, DRF asume que el cliente está usando la zona horaria especificada por TIME_ZONE y devuelve una hora formateada a esa zona horaria (agregando +5:30 al final, que en realidad no sería válido si fuera un tiempo proporcionado por el cliente). Probablemente tendría más sentido si los accesos futuros también devolvieran una hora formateada a esa zona horaria, por lo que la respuesta durante la recuperación / lista fue la misma que durante la creación / actualización.

También está la cuestión de si devolver los tiempos en la zona horaria configurada cuando se proporciona la Z final durante la creación / actualización, de modo que el envío:

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

devoluciones:

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

Yo estaría a favor de esto, ya que mantiene la respuesta consistente con la respuesta para listas / recuperaciones.

Otra opción completamente sería devolver siempre las horas UTC, incluso durante la creación / actualización, pero eso me parece menos útil. De cualquier manera, tener zonas horarias consistentes sería preferible a la situación de 50/50 que tenemos en este momento.

Configure su TIME_ZONE = 'Asia / Kolkata' y cree un serializador de modelo con un solo campo de fecha y hora, cita.

Durante la creación / actualización, envíe:

{
"cita": "2016-12-19T10: 00: 00"
}
y vuelve:

{
"cita": "2016-12-19T10: 00: 00 + 5: 30"
}
Pero si recupera o enumera ese objeto nuevamente, obtiene:

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

gracias @ jonathan-golorry, este es el comportamiento exacto que realmente veo.
Para mí, el comportamiento debería ser así (usando el ejemplo de @ jonathan-golorry :)):

Durante la creación / actualización con DATETIME_FORMAT predeterminado, envíe:

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

y vuelve:

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

Si recupera o enumera ese objeto nuevamente, obtiene:

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

En mi humilde opinión, tal vez debería ser una configuración de DRF para administrar este comportamiento, por ejemplo, una configuración para forzar que DateTimeField se represente con la zona horaria predeterminada.

muchas gracias @tomchristie

La inconsistencia entre las diferentes acciones es el factor decisivo para reabrir aquí. No me había dado cuenta de que ese era el caso. Esperaría que usemos UTC de forma predeterminada, aunque podríamos hacerlo opcional.

Para una aplicación web de producción que usa DRF 3.4.6, hemos resuelto con esta solución alternativa:
https://github.com/vpistis/django-rest-framework/commit/be62db9080b19998d4de3a1f651a291d691718f6

Si alguien quiere enviar una solicitud de extracción que incluya:

  • solo incluye casos de prueba fallidos, o ...
  • caso de prueba + arreglar

eso sería muy bienvenido.

Escribí un código para probar, pero no estoy seguro de cómo usar los casos de prueba drf. No sé cómo puedo administrar la configuración de django para cambiar la zona horaria y otras configuraciones en tiempo de ejecución.
Por favor, enlazame algún ejemplo específico o guía.

gracias

Si está buscando modificar la configuración de prueba global, se encuentran aquí .

Si está intentando anular la configuración durante la prueba, puede usar el decorador override_settings .

También agradecería esta característica. Siento que la configuración USE_TZ debe respetarse y los valores deben convertirse, de manera similar al comportamiento estándar de Django.

El primer paso aquí sería escribir un caso de prueba fallido que podamos agregar al conjunto de pruebas actual.
El siguiente paso sería iniciar una solución para ese caso :)

Hola,
para mí, este es un caso de prueba para esta función

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()

En mi bifurcación utilizo algún truco para obtener los tiempos en la zona horaria correcta mi 3.6.2_tz_fix .

Espero que esto ayude :)

Veo que esto está cerrado pero estoy ejecutando drf 3.6.3 y en mi base de datos de postgres tengo esta marca de tiempo "2017-07-12 14: 26: 00-06" pero cuando obtengo los datos usando cartero, obtengo esta "marca de tiempo" : "2017-07-12T20: 26: 00Z". Parece que está agregando las -06 horas.

Mi configuración de django usa tzlocal para establecer la zona horaria TIME_ZONE = str(get_localzone()) . Entonces, la zona horaria se establece en el inicio.

Estoy usando un modelo básicoSerializer

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

¿Me estoy perdiendo de algo?

no cerrado, todavía está abierto :)
el "botón cerrado" rojo que ve es para el problema de referencia.
... y tienes razón, el "error" sigue ahí; (
El hito se cambia de 3.6.3 a 3.6.4.

Ah, OK. ¡Gracias!

El 12 de julio de 2017 a las 17:10, "Valentino Pistis" [email protected]
escribió:

no cerrado, todavía está abierto :)
el "botón cerrado" rojo que ve es para el problema de referencia.
... y tienes razón, el "error" sigue ahí; (
El hito se cambia de 3.6.3 a 3.6.4.

-
Estás recibiendo esto porque hiciste un comentario.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/encode/django-rest-framework/issues/3732#issuecomment-314923582 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AIMBTcx-6PPbi_SOqeLCjeWV1Rb59-Ohks5sNVJ0gaJpZM4G0aRE
.

Hola,

No estoy seguro de si esto está totalmente relacionado con la pregunta original, pero DRF debería respetar cualquier anulación de zona horaria establecida, según https://docs.djangoproject.com/en/1.11/ref/utils/#django. utils.timezone.activate ?

Tengo un sistema en el que mis usuarios están asociados con una zona horaria y la API recibe fechas y horas ingenuas. Espero poder convertir esas fechas en la zona horaria del usuario actual, pero me doy cuenta de que ../rest_framework/fields.py está aplicando la zona horaria predeterminada (es decir, la del archivo de configuración de 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

¿Debería realmente usar timezone.get_current_timezone() como preferencia en caso de que la aplicación haya establecido una anulación, como en mi caso?

Hola @RichardForshaw, parece un problema distinto, pero sin duda el mismo parque de pelota.

Si podemos obtener un conjunto decente de casos de prueba que cubran el comportamiento esperado, creo que ciertamente veríamos un PR aquí.

Mi primer pensamiento está más allá de eso es asegurarme de que está enviando la información de la zona horaria a la API, en lugar de depender de la zona horaria configurada del servidor. Más allá de eso, también me inclino a pensar que debe estar preparado para localizar una hora en el cliente.

Pero sí, aquí hay una inconsistencia que debe abordarse. (¿Mencioné que las relaciones públicas son bienvenidas? 🙂)

carltongibson / django-filter # 750 debería ser relevante aquí. Originalmente basé el manejo de la zona horaria de django-filter en DRF, por lo que los cambios en 750 podrían aplicarse fácilmente aquí.

Perdón por mi novato, pero ¿cuál es exactamente el problema aquí? La marca de tiempo en mi base de datos psql es correcta y Django está configurado para usar la zona horaria correcta. ¿Existe una configuración de DRF para que no convierta las marcas de tiempo?

Hola @michaelaelise , si miras el ejemplo de los datos (cerca de la parte superior):

  1. Están enviando una fecha y hora sin información de zona horaria. (Este es un mal movimiento en mi libro).
  2. El servidor está aplicando su zona horaria local y vuelve así ( +5:30 en este caso)
  3. Pero más tarde, cuando lo recuperas, vuelve como UTC ( Z , para "Zulu", supongo).

No hay ningún problema real en el supuesto de que su cliente se encargará de la conversión de zona horaria por usted . (Excepto quizás No1, porque ¿quién puede decir que su cliente tiene la misma configuración de zona horaria que su servidor ...?)

Pero es un poco inconsistente, seguramente 2 y 3 deberían tener el mismo formato. (Aunque un cliente, correctamente, aceptará cualquiera de los dos como valores equivalentes).

Me inclino a cerrar esto.

  • Aquí no hay ningún error lógico.

    • Estos son al mismo tiempo: "2016-12-19T10:00:00+5:30" y "2016-12-19T04:30:00Z" - hasta cierto punto, ¿a quién le importa cómo regresan?

  • Por lo tanto, no es algo a lo que pueda justificar dedicar tiempo realmente.
  • El boleto tiene 2 años y nadie ha ofrecido un PR.

Estoy feliz de ver un PR, pero no estoy seguro de querer considerar esto como un _Cuestión Abierta_.

Oh, no me di cuenta de que en mi publicación original en este hilo de problemas que la "Z" significaba eso.
Entonces, ¿DRF está convirtiendo la fecha y hora consciente a UTC para que la maneje la interfaz de usuario o la persona que llama?
Gracias por esa aclaración.

Una última cosa.
¿Qué sucede si quisiéramos que se devolviera "2016-12-19T10: 00: 00 + 5: 30" porque somos dispositivos de sondeo en diferentes zonas horarias?
¿Podría tratarse de una configuración "RETURN_DATETIME_WITH_TIMEZONE"?

Estamos usando django / drf en dispositivos de borde. Por lo tanto, a todas las fechas y horas que se insertan no les importa si es ingenuo o no porque la zona horaria del dispositivo de borde está configurada y el campo de postgres siempre será preciso para la fecha y hora de ese dispositivo.

El servidor en la nube en el escenario actual necesitaría saber la zona horaria de cada dispositivo, probablemente lo hará, eso solo cambia el trabajo de django / drf a la aplicación en la nube para manejarlo.

Suponiendo USE_TZ, DRF ya está devolviendo la fecha y hora con la información de la zona horaria. Entonces ya está haciendo lo que necesita allí.

El único problema aquí es si el mismo DT está formateado como en una zona horaria u otra. (Pero siguen siendo el mismo tiempo).

@carltongibson

Aquí no hay ningún error lógico.
Estos son a la misma hora: "2016-12-19T10: 00: 00 + 5: 30" y "2016-12-19T04: 30: 00Z". Hasta cierto punto, ¿a quién le importa cómo regresan?

En mi humilde opinión, este es el problema: ¡la cadena devuelta!
Utilizo la configuración de zona horaria de Django y todas las plantillas devuelven la hora correcta "2016-12-19T10:00:00+5:30" como esperábamos, pero DRF no. DRF devuelve "2016-12-19T04:30:00Z" .
En el cliente, que consume mis apis REST, no hay lógica, no hay conversiones de tiempo o interpretación de cadenas de fecha y hora.
En otras palabras, espero que la fecha y hora de una respuesta DRF sea idéntica a la "respuesta" de la plantilla de Django: el servidor prepara todos los datos para el cliente y el cliente solo los muestra.

De todos modos, ¡muchas gracias por su paciencia, apoyo y su gran trabajo en este fantástico proyecto!

@vpistis mi punto aquí es que la fecha representada es correcta, solo que la representación no se espera. Tan pronto como lo analice con una fecha nativa, sin embargo su idioma lo maneja, no hay diferencia.

Esperaría que los usuarios analizaran la cadena de fecha en una Fecha, sin embargo, el lenguaje de su cliente lo permite, en lugar de consumir la cadena sin procesar.

Acepto que si está consumiendo la cuerda cruda, sus expectativas no se cumplirán aquí. (Pero no hagas eso: imagina si enviáramos marcas de tiempo de UNIX; no hay forma de que las consumirías sin procesar. Conviértelas en un objeto Date adecuado, lo que sea que esté en el idioma de tu cliente).

Estoy muy feliz de hacer un PR en esto. (¡Todavía no lo he cerrado!)

Pero han pasado casi dos años desde que se informó y nueve meses desde el primer comentario (el suyo, un año después). Nadie nos ha dado siquiera un caso de prueba fallido. No puede ser tan importante para nadie. Como tal, es difícil asignarle tiempo.

(Como tal, me inclino a cerrarlo sobre la base de que tomaremos un PR si alguna vez aparece uno)

Hola a todos, esto debería ser solucionado por # 5408. Si tiene tiempo para instalar la rama y verificar que todo funciona como se espera, sería fantástico. ¡Gracias!

Creo que el problema se reintrodujo de alguna manera:

Cuando cambié la TZ predeterminada de UTC a Europa / Ámsterdam, una de las pruebas falló y noté que DRF se estaba serializando a algo diferente de la TZ predeterminada

editar: el problema estaba relacionado con la prueba / configuración de fábrica.


Pruebe la configuración a continuación.

modelo

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()

fábrica

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

prueba de unidad

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

vista

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)

Serializador

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')

Resultado de la prueba de 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)])]

Resultado de la prueba

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         ?

Apilar:

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

Hola @ diegueus9 : ¿podrías reducir esto a un caso de prueba más simple? Está comparando datos falsos serializados con los datos de respuesta de una vista. Por lo tanto, no está claro cuál es el valor esperado real. Recomendaría comparar algún resultado con un valor codificado. p.ej,

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

@rpkilby gracias por tu respuesta. Fue mi error, el problema con mis pruebas unitarias es que estaba usando factory-boy / faker sin actualizar desde DB, de ahí la diferencia, acabo de agregar

    for old_something in old_somethings:
        old_something.refresh_from_db()

¿Debo eliminar mi comentario anterior o debo dejarlo en caso de que alguien más experimente el mismo falso positivo?

Hola @ diegueus9 , no te preocupes, acabo de esconder el código en una etiqueta details .

¿Fue útil esta página
0 / 5 - 0 calificaciones