如何在不重复太多的情况下处理 django DRF 中的时区?

How to handling timezones in django DRF without repeating myself too much?

# simplified functions

def localize(usertimzone, date, type):
    """
    type = 'date', 'time', 'datetime'
    """
    from dateutil import parser
    date = parser.parse(date)
    if not date.tzinfo:
        usertimzone = pytz.timezone(usertimzone)
        date = usertimzone.localize(date)
    utc_date = date.astimezone(pytz.utc)
    return utc_date

def normalize(usertimzone, date, type):
    current_user_tz = pytz.timezone(usertimzone)
    date = current_user_tz.localize(date)
    return date
#usages example
    def post(self, request, *args, **kwargs):
        alert_date = request.data.get('alert_date')
        if alert_date:
            request.data['alert_date'] = localize(request.user.timezone, alert_date, 'datetime')

注意:我的意思是通过标准化将时区从 UTC 转换为当前登录用户时区。

如 DRF 中 documentation for DateTimeField 中所述,它有一个参数 default_timezone:

default_timezone - A pytz.timezone representing the timezone. If not specified and the USE_TZ setting is enabled, this defaults to the current timezone. If USE_TZ is disabled, then datetime objects will be naive.

正如它还描述的那样,只要您设置了 USE_TZ 该字段将自动使用当前时区。这当然意味着您必须设置 (activate [Django docs]) the current timezone somehow. Django's documentation also has some sample code that uses sessions for storing the timezone and a middleware to set it, although it seems you store the timezone in the user object itself so you can write a middleware that uses that instead. Also since you use DRF for authentication, it actually does the authentication on the view layer, so the middleware does not really have the authenticated user, Since you are using rest_framework_simplejwt you can use the workaround described in this question:

import pytz

from django.utils import timezone
from rest_framework_simplejwt import authentication


class TimezoneMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        tzname = None
        user = self.get_request_user(request)
        if user:
            tzname = user.timezone
        if tzname:
            timezone.activate(pytz.timezone(tzname))
        else:
            timezone.deactivate()
        return self.get_response(request)
    
    def get_request_user(self, request):
        try:
            return authentication.JWTAuthentication().authenticate(request)[0]
        except:
            return None

将此中间件添加到 settings.pyMIDDLEWARE 列表中 AuthenticationMiddleware 之后的某处,理想情况下最后应该可以工作:

MIDDLEWARE = [
    ...
    'path.to.TimezoneMiddleware',
]

虽然上面的解决方案一开始看起来不错,但后来转向需要使用变通方法。更好的方法是使用可以设置当前时区的混合。 ApiView class 的 initial 方法是我们 mixin 完成任务的一个好点,这个方法在实际视图方法之前被调用(getpost, 等等) 被调用所以它适合我们的需要:

import pytz

from django.utils import timezone


class TimezoneMixin:
    def initial(self, request, *args, **kwargs):
        super().initial(request, *args, **kwargs)
        tzname = None
        if request.user.is_authenticated:
            tzname = request.user.timezone
        if tzname:
            timezone.activate(pytz.timezone(tzname))
        else:
            timezone.deactivate()


class YourView(TimezoneMixin, SomeViewClassFromDRF):
    ...