Django-rest-framework: Serializer DateTimeField tem informações inesperadas de fuso horário

Criado em 13 dez. 2015  ·  34Comentários  ·  Fonte: encode/django-rest-framework

Tenho TIME_ZONE = 'Asia/Kolkata' e USE_TZ = True em minhas configurações.

Quando eu crio um novo objeto com a API navegável, o serializador exibe o objeto recém-criado com data e hora que possuem +5:30 à direita, indicando o fuso horário. O banco de dados está armazenando os horários em UTC.

O comportamento inesperado é que, quando o objeto é serializado novamente mais tarde, os horários de dados estão todos no formato UTC com Z à direita. De acordo com os documentos do Django sobre fuso horário atual e padrão, eu esperaria que o serializador convertesse os datetimes para o fuso horário atual, que é padronizado para o fuso horário padrão, que é definido por TIME_ZONE = 'Asia/Kolkata' .

Estou esquecendo de algo?

Needs further review

Comentários muito úteis

Também apreciaria esse recurso. Eu realmente sinto que a configuração USE_TZ deve ser respeitada e os valores devem ser convertidos, semelhante ao comportamento padrão do Django.

Todos 34 comentários

Eu perguntei sobre o estouro de pilha e a conclusão é que o fuso horário atual é usado apenas para data e hora fornecidas pelo usuário, pois a data e hora é serializada como está após a validação (ou seja, após a criação ou atualização). Pela documentação do Django, acho difícil acreditar que esse seja o comportamento pretendido, mas esse é um problema com o próprio Django e não com o Rest Framework, portanto, encerrarei esse problema.

Olá @tomchristie ,

Acho que é um bug DRF.
IMHO, o uso natural do DRF é usá-lo como uma saída como um modelo de django.
Em um modelo django o fuso horário é mostrado corretamente, por que o serializador drf não respeita a configuração de fuso horário django?

Oi,

olhando para o código em fields.py na classe DatetimeField , descubro que a configuração de fuso horário do Django é bem considerada apenas para o valor interno ( to_internal_value() ).

Eu vi que usar o serializador de modelo assim:

class MyModelSerializer(ModelSerializer):

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

para o campo my_date_time_field (que eu acho que é um DateTimeField :)) o fuso horário para a representação é Nenhum por padrão ( to_representation() ).

Em outras palavras, se não estou errado, o fuso horário do Django é considerado apenas quando o valor é gravado no back-end de armazenamento.

IMHO, acho que o valor de retorno de to_representation() deve ser assim:
return self.enforce_timezone(value).strftime(output_format)
de acordo com a configuração do Django USE_TZ .

Eu escrevo uma solicitação de pull para isso.

tchau
Valentino

@vpistis Seria mais fácil revisar isso se você pudesse dar um exemplo do comportamento da API que você vê atualmente e o que você esperaria ver (em vez de discutir os aspectos de implementação primeiro)

Defina seu TIME_ZONE = 'Asia/Kolkata' e crie um serializador de modelo com um único campo de data e hora, appointment .

Durante a criação / atualização, envie:

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

e volte:

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

Mas se você recuperar ou listar esse objeto novamente, você obterá:

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

Durante a criação, se o Z não for especificado, o DRF assume que o cliente está usando o fuso horário especificado por TIME_ZONE e retorna um horário formatado para esse fuso horário (adicionando +5:30 no final, que seria realmente inválido se fosse um horário fornecido pelo cliente). Provavelmente faria mais sentido se os acessos futuros também retornassem um horário formatado para esse fuso horário, de forma que a resposta durante a recuperação / lista fosse a mesma que durante a criação / atualização.

Também há a questão de se retornar os horários no fuso horário configurado quando o Z final é fornecido durante a criação / atualização, como o envio:

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

retorna:

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

Eu ficaria por isso, pois mantém a resposta consistente com a resposta para listas / recuperações.

Outra opção totalmente seria sempre retornar os horários UTC, mesmo durante a criação / atualizações, mas acho isso menos útil. De qualquer forma, ter fusos horários consistentes seria preferível à situação 50/50 que temos agora.

Defina seu TIME_ZONE = 'Asia / Kolkata' e crie um serializador de modelo com um único campo de data e hora, compromisso.

Durante a criação / atualização, envie:

{
"compromisso": "2016-12-19T10: 00: 00"
}
e volte:

{
"compromisso": "2016-12-19T10: 00: 00 + 5: 30"
}
Mas se você recuperar ou listar esse objeto novamente, você obterá:

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

obrigado @ jonathan-golorry este é o comportamento exato que eu realmente vejo.
Para mim, o comportamento deve ser assim (usando o exemplo @ jonathan-golorry :)):

Durante a criação / atualização com DATETIME_FORMAT padrão, envie:

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

e volte:

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

Se você recuperar ou listar esse objeto novamente, obterá:

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

IMHO talvez deva ser uma configuração DRF para gerenciar esse comportamento, por exemplo, uma configuração para forçar o DateTimeField a ser representado com o fuso horário padrão.

muito obrigado @tomchristie

A inconsistência entre diferentes ações é o argumento decisivo para reabrir aqui. Eu não tinha percebido que era o caso. Eu esperaria que usássemos o UTC por padrão, embora pudéssemos tornar isso opcional.

Para um aplicativo da web de produção que usa DRF 3.4.6, resolvemos com esta solução alternativa:
https://github.com/vpistis/django-rest-framework/commit/be62db9080b19998d4de3a1f651a291d691718f6

Se alguém quiser enviar uma solicitação pull que inclua:

  • apenas inclui casos de teste com falha, ou ...
  • caso de teste + correção

isso seria muito bem-vindo.

Eu escrevi alguns códigos para testar, mas não tenho certeza de como usar casos de teste drf. Eu não sei como posso gerenciar as configurações do django para alterar o fuso horário e outras configurações em tempo de execução.
Por favor, ligue-me algum exemplo ou guia específico.

obrigado

Se você deseja modificar as configurações globais de teste, elas estão localizadas aqui .

Se estiver tentando substituir as configurações durante o teste, você pode usar o decorador override_settings .

Também apreciaria esse recurso. Eu realmente sinto que a configuração USE_TZ deve ser respeitada e os valores devem ser convertidos, semelhante ao comportamento padrão do Django.

O primeiro passo aqui seria escrever um caso de teste com falha que podemos adicionar ao conjunto de testes atual.
O próximo passo seria iniciar uma correção para esse caso :)

Oi,
para mim, este é um caso de teste para este recurso

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

No meu fork, eu uso algum truque para obter os horários no fuso horário correto no meu 3.6.2_tz_fix .

Espero que ajude :)

Vejo que está fechado, mas estou executando o drf 3.6.3 e em meu banco de dados postgres tenho este carimbo de data / hora "2017-07-12 14: 26: 00-06", mas quando obtenho os dados usando o carteiro recebo este "carimbo de data / hora" : "2017-07-12T20: 26: 00Z". Parece que está adicionando -06 horas.

Minhas configurações do django usam tzlocal para definir o fuso horário TIME_ZONE = str(get_localzone()) . Portanto, o fuso horário é definido na inicialização.

Estou usando um modelSerializer básico

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

Estou esquecendo de algo?

não está fechado, ainda está aberto :)
o "botão fechado" vermelho que você vê é para o problema de referência.
... e você está certo, o "bug" ainda está lá; (
O marco é alterado do formulário 3.6.3 para o 3.6.4.

Ah ok. Obrigado!

Em 12 de julho de 2017, 17:10, "Valentino Pistis" [email protected]
escreveu:

não está fechado, ainda está aberto :)
o "botão fechado" vermelho que você vê é para o problema de referência.
... e você está certo, o "bug" ainda está lá; (
O marco é alterado do formulário 3.6.3 para o 3.6.4.

-
Você está recebendo isso porque comentou.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/encode/django-rest-framework/issues/3732#issuecomment-314923582 ,
ou silenciar o tópico
https://github.com/notifications/unsubscribe-auth/AIMBTcx-6PPbi_SOqeLCjeWV1Rb59-Ohks5sNVJ0gaJpZM4G0aRE
.

Oi,

Não tenho certeza se isso está totalmente relacionado à questão original, mas o DRF deve honrar quaisquer substituições de fuso horário definidas, conforme https://docs.djangoproject.com/en/1.11/ref/utils/#django. utils.timezone.activate ?

Tenho um sistema em que meus usuários estão associados a um fuso horário e a API recebe datetimes ingênuos. Espero ser capaz de converter esses horários no fuso horário do usuário atual, mas noto que ../rest_framework/fields.py está aplicando o fuso horário padrão (ou seja, aquele do arquivo de configuração 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

Isso realmente deveria estar usando timezone.get_current_timezone() como preferência, caso o aplicativo tenha definido uma substituição, como no meu caso?

Olá, @RichardForshaw, isso parece um problema distinto, mas certamente é o mesmo.

Se pudermos obter um conjunto decente de casos de teste cobrindo o comportamento esperado, acho que certamente veríamos um PR aqui.

Meu primeiro pensamento é além disso, certifique-se de enviar as informações de fuso horário para a API, em vez de depender do fuso horário configurado do servidor. Além disso, também estou inclinado a pensar que você precisa estar preparado para localizar um momento no cliente.

Mas sim, há uma inconsistência aqui a ser tratada. (Eu mencionei as boas-vindas dos RP? 🙂)

carltongibson / django-filter # 750 deve ser relevante aqui. Eu originalmente baseei o manuseio de fuso horário do django-filter no DRF, então as mudanças em 750 podem ser facilmente aplicadas aqui.

Desculpe pela minha novidade, mas qual é exatamente o problema aqui? O carimbo de data / hora em meu banco de dados psql está correto e o Django está configurado para usar o fuso horário correto. Existe uma configuração de DRF apenas para torná-lo não converter carimbos de data / hora?

Olá @michaelaelise - se você olhar o exemplo dos dados (próximo ao topo):

  1. Eles estão enviando uma data e hora sem informações de fuso horário. (Esta é uma jogada ruim no meu livro.)
  2. O servidor está aplicando seu fuso horário local e ele retorna assim ( +5:30 neste caso)
  3. Mais tarde, quando você o busca, ele retorna como UTC ( Z , para "Zulu", suponho).

Não há nenhum problema real em supor que seu cliente fará a conversão de fuso horário para você . (Exceto talvez No1, porque quem pode dizer que seu cliente tem a mesma configuração de fuso horário que seu servidor ...?)

Mas é um pouco inconsistente, certamente 2 e 3 deveriam ter o mesmo formato? (Mesmo que um cliente aceite, corretamente, qualquer um como valores equivalentes.)

Estou inclinado a fechar isso.

  • Não há nenhum erro lógico aqui.

    • São ao mesmo tempo: "2016-12-19T10:00:00+5:30" e "2016-12-19T04:30:00Z" - até certo ponto, quem se importa como eles voltam?

  • Portanto, não é algo que eu possa realmente justificar alocar tempo.
  • O ingresso tem 2 anos e ninguém ofereceu um PR.

Estou feliz em ver um PR, mas não tenho certeza se quero realmente considerar isso uma _ Edição aberta_.

Oh, eu não percebi que em minha postagem original neste tópico de discussão que o "Z" significava isso.
Portanto, o DRF está convertendo a data e hora ciente em UTC para ser tratada pela IU / chamador?
Obrigado pelo esclarecimento.

Uma última coisa.
E se quisermos que "2016-12-19T10: 00: 00 + 5: 30" seja retornado porque estamos pesquisando dispositivos em diferentes fusos horários.
Poderia ser uma configuração "RETURN_DATETIME_WITH_TIMEZONE"?

Estamos usando django / drf em dispositivos de ponta. Portanto, todos os datetimes sendo inseridos não se importam se são ingênuos ou não, porque o fuso horário do dispositivo de borda está configurado e o campo postgres sempre será preciso para o datetime desse dispositivo.

O servidor em nuvem no cenário atual, então, precisaria saber o fuso horário de cada dispositivo, provavelmente o fará, que apenas muda o trabalho de django / drf para o aplicativo em nuvem para lidar.

Assumindo USE_TZ, o DRF já está retornando os horários das datas com as informações de fuso horário. Portanto, já está fazendo o que você precisa.

O único problema aqui é se o mesmo DT está formatado como em um fuso horário ou outro. (Mas eles ainda são o mesmo tempo.)

@carltongibson

Não há nenhum erro lógico aqui.
São ao mesmo tempo: "2016-12-19T10: 00: 00 + 5: 30" e "2016-12-19T04: 30: 00Z" - até certo ponto, quem se importa como eles voltam?

IMHO este é o problema: a string retornada!
Eu uso as configurações de fuso horário do Django e todos os modelos retornam a hora correta "2016-12-19T10:00:00+5:30" como esperávamos, mas o DRF não. Retorno DRF "2016-12-19T04:30:00Z" .
No cliente, que consome minhas apis REST, não há lógica, nenhuma conversão de tempos ou interpretação de strings de data e hora.
Em outras palavras, espero que a data e hora de uma resposta DRF seja idêntica à "resposta" do Django Template: o servidor prepara todos os dados para o cliente e o cliente apenas os mostra.

De qualquer forma, muito obrigado pela paciência, apoio e seu ótimo trabalho neste fantástico projeto!

@vpistis, meu ponto aqui é apenas que a data representada está correta, apenas a representação não é esperada. Assim que você analisa isso para uma data nativa, independentemente de como seu idioma lida com isso, não há diferença.

Eu esperaria que os usuários analisassem a string de data para uma Date, no entanto, sua linguagem cliente fornece isso, em vez de consumir a string bruta.

Aceito que, se você estiver consumindo a string crua, suas expectativas não serão atendidas aqui. (Mas não faça isso: imagine se enviássemos carimbos de data / hora UNIX; não há como você consumi-los brutos. Converta em um objeto Date adequado, seja qual for o idioma de seu cliente.)

Estou muito feliz em fazer um RP sobre isso. (Eu não fechei ainda!)

Mas já se passaram quase dois anos desde o relatório e nove meses desde o primeiro comentário (o seu, um ano depois). Ninguém nos deu um caso de teste com falha. Não pode ser tão importante para ninguém. Como tal, é difícil alocar tempo.

(Como tal, estou inclinado a fechá-lo com base em que faremos um PR, se algum dia aparecer)

Olá a todos, isso deve ser corrigido em # 5408. Se você tiver tempo para instalar o branch e verificar se tudo está funcionando conforme o esperado, isso seria fantástico. Obrigado!

Acho que o problema foi, de alguma forma, reintroduzido:

Quando mudei o TZ padrão de UTC para Europa / Amsterdã, um dos testes falhou e percebi que o DRF está serializando para algo diferente do TZ padrão

editar: o problema estava relacionado ao teste / configuração de fábrica.


Configuração de teste abaixo.

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

teste de unidade

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

visualizar

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 do teste 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 do teste

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         ?

Pilha:

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

Olá @ diegueus9 - você poderia reduzir isso para um caso de teste mais simples? Você está comparando dados falsos serializados com os dados de resposta de uma visualização. Portanto, não está claro qual é o valor real esperado. Eu recomendo comparar algum resultado com um valor codificado. por exemplo,

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

@rpkilby obrigado pela sua resposta. Foi um erro meu, o problema com meus testes de unidade é que eu estava usando o factory-boy / faker sem atualizar do DB, daí a diferença, acabei de adicionar

    for old_something in old_somethings:
        old_something.refresh_from_db()

Devo remover meu comentário anterior ou devo deixá-lo para o caso de outra pessoa ter o mesmo falso positivo?

Olá @ diegueus9 , não se preocupe - acabei de ocultar o código em uma tag details .

Esta página foi útil?
0 / 5 - 0 avaliações