Django-rest-framework: Serializer DateTimeField 有意外的时区信息

创建于 2015-12-13  ·  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 错误。
恕我直言,DRF 的自然用途是将其用作像 django 模板一样的输出。
在 django 模板中时区正确显示,为什么 drf 序列化程序不尊重 django 时区设置?

你好,

DatetimeField类中查看fields.py中的代码,我发现 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 时区。

恕我直言,我认为to_representation()的返回值应该是这样的:
return self.enforce_timezone(value).strftime(output_format)
根据 Django USE_TZ设置。

我为此写了一个拉取请求。

再见
华伦天奴

@vpistis如果您可以举例说明您当前看到的 API 行为以及您希望看到的内容(而不是首先讨论实现方面),那么查看此内容会更容易

设置您的TIME_ZONE = 'Asia/Kolkata'并使用单个日期时间字段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' 并创建一个带有单个日期时间字段的模型序列化程序,约会。

在创建/更新期间,发送:

{
"约会": "2016-12-19T10:00:00"
}
然后回来:

{
"约会": "2016-12-19T10:00:00+5:30"
}
但是,如果您再次检索或列出该对象,则会得到:

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

thanx @jonathan-glorry 这是我实际看到的确切行为。
对我来说,行为应该是这样的(使用@jonathan-glorry 示例:)):

在使用默认 DATETIME_FORMAT 创建/更新期间,发送:

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

然后回来:

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

如果您再次检索或列出该对象,您将得到:

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

恕我直言,可能应该是一个 DRF 设置来管理此行为,例如,强制使用默认时区表示 DateTimeField 的设置。

非常感谢@tomchristie

不同行动之间的不一致是这里重新开放的关键。 我没有意识到是这样。 我希望我们在默认情况下始终使用 UTC,尽管我们可以将其设为可选。

对于使用 DRF 3.4.6 的生产 Web 应用程序,我们已经解决了以下解决方法:
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()

在我的 fork 中,我使用了一些技巧来在我的 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()) 。 所以时区是在启动时设置的。

我正在使用基本的 modelSerializer

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。

哦好的。 谢谢!

2017 年 7 月 12 日下午 5:10,“Valentino Pistis”通知@github.com
写道:

没有关闭,它仍然开放:)
您看到的红色“关闭按钮”用于参考问题。
...你是对的,“错误”仍然存在;(
里程碑从 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尊重设置的任何时区覆盖

我有一个系统,其中我的用户与时区相关联,并且 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 在这里应该是相关的。 我最初在 DRF 上基于 django-filter 的时区处理,所以 750 中的更改可以很容易地应用在这里。

对不起,我的新手,但这里的问题究竟是什么? 我的 psql 数据库中的时间戳是正确的,并且 Django 设置为使用正确的时区。 是否有 DRF 设置使其不转换时间戳?

@michaelaelise - 如果您查看数据示例(靠近顶部):

  1. 他们正在发送没有时区信息的日期时间。 (这在我的书中是一个糟糕的举动。)
  2. 服务器正在应用其本地时区,然后返回(在这种情况下为+5:30
  3. 但是稍后,当您获取它时,它会作为 UTC 返回(我想是Z ,对于“祖鲁语”)。

假设您的客户将为您处理时区转换,这没有实际问题。 (除了No1,因为谁能说您的客户端与您的服务器具有相同的时区设置......?)

但是有点不一致,2和3肯定应该有相同的格式吗? (即使客户端会正确地接受任何一个作为等效值。)

我倾向于关闭这个。

  • 这里没有逻辑错误。

    • 这些是同一时间: "2016-12-19T10:00:00+5:30""2016-12-19T04:30:00Z" ——在某种程度上,谁在乎他们是怎么回来的?

  • 因此,这不是我可以真正分配时间的理由。
  • 这张票是2年的,没有人提供过PR。

我很高兴看到 PR,但我不确定我是否真的想将其视为 _Open Issue_。

哦,我没有意识到在这个问题线程的原始帖子中,“Z”是这个意思。
那么 DRF 正在将感知日期时间转换为 UTC 以由 UI/调用者处理?
谢谢你的澄清。

最后一件事。
如果我们希望返回 "2016-12-19T10:00:00+5:30" 怎么办,因为我们正在轮询不同时区的设备。
这可能是设置“RETURN_DATETIME_WITH_TIMEZONE”吗?

我们在边缘设备上使用 django/drf。 因此,所有插入的日期时间都不关心它是否幼稚,因为配置了边缘设备时区并且 postgres 字段对于该设备的日期时间始终是准确的。

当前场景中的云服务器需要知道每个设备的时区,它可能会,这只是将工作从 django/drf 转移到云应用程序来处理。

假设 USE_TZ,DRF 已经返回带有时区信息的日期时间。 所以它已经在做你需要的事情了。

这里唯一的问题是相同 DT 的格式是在一个时区还是在另一个时区。 (但它们仍然是同一时间。)

@卡尔顿吉布森

这里没有逻辑错误。
这些是同一时间:“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"
进入客户端,消耗我的 REST api,没有逻辑,没有时间转换或日期时间字符串解释。
换句话说,我希望来自 DRF 响应的日期时间与 Django 模板“响应”相同:服务器为客户端准备所有数据,客户端只显示它。

无论如何,非常感谢您对这个出色项目的耐心、支持和出色的工作!

@vpistis我在这里的观点只是表示的日期是正确的,只是表示不是预期的。 一旦您将其解析为本地日期,无论您的语言如何处理,都没有区别。

我希望用户将日期字符串解析为日期,但是他们的客户端语言提供了这一点,而不是使用原始字符串。

如果您正在使用原始字符串,我接受您的期望不会在这里得到满足。 (但不要那样做:想象一下,如果我们发送 UNIX 时间戳;您无法使用这些原始数据。转换为适当的 Date 对象,无论您的客户端语言是什么。)

我真的很高兴对此进行 PR。 (我还没关闭呢!)

但是距离报道已经快两年了,距离第一次评论(你的,一年后)已经过去了九个月。 甚至没有人给我们一个失败的测试用例。 对任何人都没有那么重要。 因此很难分配时间。

(因此,我倾向于关闭它,因为如果有人出现我们将采取 PR)

大家好,这应该由#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 而没有从 DB 刷新,因此不同,我刚刚添加

    for old_something in old_somethings:
        old_something.refresh_from_db()

我应该删除我之前的评论还是我应该保留它以防其他人遇到同样的误报?

@diegueus9 ,不用担心 - 我只是将代码隐藏在details标签中。

此页面是否有帮助?
0 / 5 - 0 等级