The strategy to be applied for the correct work of timezones
Getting up at six in the morning is easy.
The main thing is to choose a convenient time zone.
Sooner or later, everyone is faced with the problem of time zones in the project. This is quite natural: people from different parts of the world can use your project. Consequently, we need to ensure the correct display of the date and time. The questions need to be addressed:
- How to get the user’s current timezone and what to do with it?
- How to convert date and time?
- How to store date and time in the database?
- What format to return the date and time to the frontend in?
- What should the frontend do with the received date?
To answer these questions we will provide an example. Let’s say we have a project (Django/React), which is used by people from different countries and in different time zones. For each user, we need to display data in the correct timezone.
We can get the user’s timezone on the frontend. One might suspect, since we can get a timezone at the frontend, this means that we can easily convert the date and time into the right timezone there. But there is a problem. When the frontend receives the date and time from the backend, it does not know which timezone this date and time are in. It can be a timezone of a server or another user. Also, when we send the date and time, it is not clear in what time zone it is necessary to send it to the backend. Therefore, it is better to shift these responsibilities to the backend, while giving the backend all the necessary information, namely the time zone of the current user.
One way to do this is to send the current timezone in the request headers.
import { create } from "apisauce";
import { BASE_URL } from "./endpoints";
const api = create({ baseURL: BASE_URL });
const setHeaders = api => {
api.addRequestTransform(request => {
request.headers[
"Client-Location"
] = Intl.DateTimeFormat().resolvedOptions().timeZone;
});
}
setHeaders(api);
export default api;
Please note instead of simply adding an offset (for example, we have the Kyiv time zone, that is, it will be +03: 00), we send the name of the time zone (Europe/Kyiv). So we accurately take into account summer and wintertime.
And further, in each request, we use this wrapper.
var urlParts = window.location.href.split("/");
export const BASE_URL = `${urlParts[0]}//${urlParts[2]}/api/`;
export const LOGIN = `$login/`;
auth.js
import api from "./api";
import { LOGIN } from "./endpoints";
export const login = data => api.post(LOGIN, data);
Now we have in each request a timezone of the current user. It is convenient to have one mutual control point both on the frontend and backend. To do this, we employ a middleware, which will deliver a timezone from the request.
from django.utils.deprecation import MiddlewareMixin
from threading import local
TIMEZONE_ATTR_NAME = "_current_timezone"
_thread_locals = local()
def get_current_timezone():
"""
Get current timezone to thread local storage.
"""
return getattr(_thread_locals, TIMEZONE_ATTR_NAME, None)
class ThreadLocalMiddleware(MiddlewareMixin):
"""
Middleware that gets timezone from the
request and saves it in thread local storage.
"""
def process_request(self, request):
# Handles current user timezone.
current_timezone = request.META.get("HTTP_CLIENT_LOCATION", "UTC")
setattr(_thread_locals, TIMEZONE_ATTR_NAME, current_timezone)
Remember to add it to settings.py
MIDDLEWARE = [
…,
'user.middleware.ThreadLocalMiddleware',
]
Now, when we receive data or send it to the frontend, we can convert it to the time zone we need.
When the frontend sends us the date and time, from the request headers we understand in which timezone it came. Since the date and time in the database are saved without a timezone, before writing this date and time to the database, it would be nice to convert it into a timezone in which we will save it in the database. This is often UTC-0. Therefore, for convenience, we add functions that will convert the date and time from UTC-0 to the user’s timezone and vice versa.
import pytz
from middleware import get_current_timezone
def convert_datetime_to_user_timezone(date_time):
"""
Convert date to current user timezone.
"""
current_timezone = get_current_timezone()
local = pytz.timezone(current_timezone)
return date_time.astimezone(local)
def convert_datetime_to_utc_timezone(date_time):
"""
Convert date to utc timezone.
"""
user_tz = pytz.timezone(get_current_timezone())
localized_time = user_tz.localize(date_time.replace(tzinfo=None))
return localized_time.astimezone(pytz.utc)
In most cases, when we need to save the date and time to the database, this is the current time. For example, when the record was last updated, publication time, creation time, and so on. To do this, we set the default value timezone.now for the date and time field in the model.
from django.db import models
class Message(models.Model):
title = models.CharField(max_length=255)
message = models.TextField()
publish_datetime = models.DateTimeField(default=timezone.now)
Note that for the default value, we use timezone.now, not datetime.now.
When we take date and time from the database, before sending it to the frontend, we need to transfer the date and time to the timezone of the user who requested the data. To do this, in the serializer we change the representation of date and time.
from rest_framework import serializers
from models import Message
class MessageSerializer(serializers.ModelSerializer):
class Meta:
model = Message
fields = ["title", "message", "publish_datetime"]
def to_representation(self, instance):
data = super().to_representation(instance)
date_time = convert_datetime_to_user_timezone(instance.publish_datetime)
data["publish_datetime"] = date_time.strftime("%Y-%m-%d %H:%M")
return data
But what remains to be done by the frontend? When it receives date and time, it only needs to display to the user.
Also, do not forget that in addition to recording and displaying the date and time, we can have filters in the project by date and time. For example, we want to receive our messages in a certain period.
To do this, the frontend sends the date, time and current time zone of the user, while the backend is already doing all the rest of the magic.
from django_filters import rest_framework as filters
from models import Message
from utils import convert_datetime_to_utc_timezone
class DateTimeFromToRangeFilter(filters.DateTimeFromToRangeFilter):
def filter(self, qs, value):
if value:
start = convert_datetime_to_utc_timezone(value.start) if value.start else None
stop = convert_datetime_to_utc_timezone(value.stop) if value.stop else None
value = slice(start, stop, None)
qs = super().filter(qs, value)
return qs
class MessageFilter(filters.FilterSet):
publish_datetime = DateTimeFromToRangeFilter(field_name="publish_datetime", lookup_expr='gt')
class Meta:
model = Message
fields = ["publish_datetime"]
Now users can easily use our application in different time zones. This is all done in one place on the backend, and the frontend does not perform any unnecessary operations on the client-side.
From this article we can learn the following:
– the database stores the date and time without a timezone;
– the backend ensures that the date and time are processed correctly;
– the frontend reports which timezone it is in, and then it simply displays the date it received.
I hope this article was useful to you. Like and subscribe to our channel =)