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

1

4
  • Intro: My project TIME_ZONE is equal to 'UTC' while I have users from too many time zones. So, when I user make POST or PUT with date or time or dateTime fields I convert these fields to UTC before serializer.save(). Then, when a user make a GET request I convert the same fields back to the timezone of the user which is request.user.timezone
# 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')
  • Problem: I used these function in too many views and every time I create a new view I use it again.
  • Goal: I need to find a way to do that in one function that generalize this conversion for all dateField, timeField and datetimeField fields.
  • I tried: to make a class view and use these functions inside it then override that class for each new app. But, each model have date fields with different names and it was hard to make a flexible function that recognize which field need to be localized or normalized.

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

Surmise answered 17/7, 2021 at 6:56 Comment(0)
W
5

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):
    ...
Wintergreen answered 17/7, 2021 at 7:28 Comment(9)
I got request.user.is_authenticated always returns False?Surmise
@alial-karaawi oh wait, do you use authentication from DRF? That might need some modifying.Wintergreen
I am using 'rest_framework_simplejwt.authentication.JWTAuthentication'Surmise
@alial-karaawi check the edit, used a workaround described hereWintergreen
Oh perfect thank you so much, I never kn3w that I get get user with authentication.JWTAuthentication().authenticate(request)Surmise
@alial-karaawi although this is a workaround (authentication code gets called twice), perhaps a better way would be to create a mixin or a base class which all your views would inherit from (or a decorator for function based views).Wintergreen
I tried the create a general class and override it for each view but things was interferingSurmise
Let us continue this discussion in chat.Surmise
works like a py-charm, thanksDameron

© 2022 - 2025 — McMap. All rights reserved.