djangodjango-rest-frameworktimezonepytzdjango-timezone

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

Note: I mean by normalized to convert the timezone from UTC the the current login-in user timezone.


Solution

  • As described in the documentation for DateTimeField in DRF, it has an argument 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.

    As it also describes as long as you have set USE_TZ the field will automatically use the current timezone. This of course means you would have to set (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
    

    Add this middleware to the MIDDLEWARE list in settings.py somewhere after AuthenticationMiddleware, ideally at the last should work:

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

    Although the above solution seemed good at the start, it later turned to needing to use a workaround. A better way would be to use a mixin that would set the current timezone. A good point for our mixin to do it's task would be the initial method of the ApiView class, this method is called just before the actual view method (get, post, etc.) is called so it suits our needs:

    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):
        ...