DjangoRestFramework ModelSerializer DateTimeField

2019-04-25 11:58发布

Edit: This has now been recognized as a bug and it looks like a fix is in progress: https://github.com/tomchristie/django-rest-framework/issues/3732#issuecomment-267635612

I have a Django project where I expect the user to be in a certain timezone. I have TIME_ZONE = 'Asia/Kolkata' and USE_TZ = True in my settings.

I have a model that includes a datetimefield. When I first create the object, the modelserializer gives datetimes with a trailing +5:30. Annoyingly, datetimes with auto_now_add=True give UTC datetimes with a trailing Z. I fixed this by making the field's default a callable for the current time.

If I serialize the object again at any point, all datetimes are in UTC with a trailing Z. From the Django documentation, I would expect the serializer to use the current timezone, which defaults to the default timezone set by TIME_ZONE = 'Asia/Kolkata'. I have checked the current timezone in my view with get_current_timezone_name() and it is 'Asia/Kolkata'. I have even tried using activate('Asia/Kolkata') in my view, but times are still being returned in UTC.

Note that all the times are correct (the UTC times are 5:30 hours earlier), its just that I would expect for the times to be converted. All datetimes are stored in the DB as UTC times as expected.

Am I missing something or is this a bug with the Django Rest Framework serializers?

1条回答
家丑人穷心不美
2楼-- · 2019-04-25 12:39

Have a look at the documentation here: http://www.django-rest-framework.org/api-guide/fields/#datetimefield

Signature: DateTimeField(format=None, input_formats=None)

format - A string representing the output format. If not specified, this defaults to the same value as the DATETIME_FORMAT settings key, which will be 'iso-8601' unless set. Setting to a format string indicates that to_representation return values should be coerced to string output. Format strings are described below. Setting this value to None indicates that Python datetime objects should be returned by to_representation. In this case the datetime encoding will be determined by the renderer.

When a value of None is used for the format datetime objects will be returned by to_representation and the final output representation will determined by the renderer class.

In the case of JSON this means the default datetime representation uses the ECMA 262 date time string specification. This is a subset of ISO 8601 which uses millisecond precision, and includes the 'Z' suffix for the UTC timezone, for example: 2013-01-29T12:34:56.123Z.

So getting the UTC (Z) representation of your datetime object is in fact default behaviour.

When you create or update a model instance through Djangorest, the serializer will be called with the data kwarg, which doesn't happen if you use the list or detail view.

In both cases, your view will return serializer.data. In case of a create/update operation, this will be a representation of serializer.validated_data, while in case of a list/detail operation it'll be a direct representation of the instance.

In both cases, the representation is achieved by calling field.to_representation with the default kwarg format=None, which will make the field return the plain string value.

The magic happens here:

  • create/update: The validation returns a timezone aware object, which includes your standard timezone. It is converted to string by calling its isoformat() method, and returned as-is.
  • list/retrieve: The django ORM stores the timestamp as UTC. It is converted to string by calling its isoformat() method, but DRF replaces +00:00 with Z (see the link above).

So to get the desired output with timezone offset, you could pass format=None or a custom strftime string to the DateTimeField in your serializer. However, you will always get the UTC time with +00:00, because that's what the data is (fortunately) stored as.

If you want the actual offset to 'Asia/Kolkata', you will probably have to define your own DateTimeField:

from django.utils import timezone
class CustomDateTimeField(serializers.DateTimeField):
    def to_representation(self, value):
        tz = timezone.get_default_timezone()
        # timezone.localtime() defaults to the current tz, you only
        # need the `tz` arg if the current tz != default tz
        value = timezone.localtime(value, timezone=tz)
        # py3 notation below, for py2 do:
        # return super(CustomDateTimeField, self).to_representation(value)
        return super().to_representation(value) 
查看更多
登录 后发表回答