📜 ⬆️ ⬇️

Django and time zones

There are several ordinary things that from time to time spoil the blood of our brother: cases, numerals and time zones, with a damned transition to summer / winter time. Involuntarily you will envy the Chinese who have only one time zone for the whole country, and there are no trace of cases. It will be quite good to once and for all deal with time zones and transformations between them, at least for Django applications.

In Python itself, this is not bad, there is an excellent pytz module, and the built-in datetime object works correctly with time zones. Things are easy - to implement a convenient strapping. The first thing that comes to mind is to write a localtime template filter and call it like this:

{{ comment.time_added | localtime }}
{{ comment.time_added | localtime | date :"dmY" }}

But immediately a couple of problems arise. First, the filter function does not take context, and therefore will not be able to determine to which time zone to bring time. Secondly, the current time zone can be understood in different ways: taken from the settings of the current user, determined by the selected city or by IP. Those. what is now a local time zone depends on the application and, accordingly, such a filter will need to be constantly rewritten. And thirdly, all these parameters need to be passed into the context.

The first problem is solved by using the tag instead of the filter (it can get the context), though it will not look so beautiful:
')
{% localtime comment.time_added %}
{% localtime comment.time_added as time_added %}{{ time_added | date :"dmY" }}

Especially risky and impatient connoisseurs of beauty can use the patch , it just allows you to transfer the context to the filter.

There are troubles with the context, you can write your context processor , and you can use the standard django.core.context_processors.request and fill its timezone property with the help of Middleware:

class TimezoneMiddleware ( object ):
"""
request timezone
"""

def process_request ( self , request):
assert hasattr (request, 'session' ), "*.TimezoneMiddleware requires session middleware to be installed."
request . timezone = get_timezone(request)
request . session[ 'timezone' ] = request . timezone
return None

Dependence on the session middleware can be removed if you are not going to cache the time zone in the session. The get_timezone() function will depend on the application and may look like this:

def get_timezone (request):
#
if hasattr (request, 'user' ) and request . user . is_authenticated():
profile = request . user . get_profile()
if profile . timezone:
return profile . timezone

#
if request . session . has_key( 'timezone' ):
return request . session[ 'timezone' ]

# IP,
city_id = ip_to_city_id(request . META[ 'REMOTE_ADDR' ])
if city_id:
try :
city = City . objects . get(pk = city_id)
if city . timezone:
return city . timezone
except City . DoesNotExist:
pass

#
return pytz . timezone(settings . FALLBACK_TIMEZONE)

Actually, it would be possible to give the code for the tag and the template filter and it will round out, but a professionally lazy programmer, like me, decides that writing a tag or a localtime filter is troublesome every time, plus when issuing forms you need to manually convert the time in the fields there and back, plus in the absence of the request context (sending letters by crown, for example) it will not work without additional gestures, plus you need to be constantly alert when working in views - the calendar with events may look different for different time zones. Well, hardworking guys can take the filter code in the example to the above patch and be like this, let the rest be ready for a little witchcraft.

Obviously, if we want dates and times to be automatically transferred to the current time zone, then we really can't do without some magic. All the data from the models we get through their fields is excellent, transforming the time after sampling and before inserting you can get the desired effect. However, the fields do not know anything about the context of the template, or about the request object, they may not exist at all. Obviously, an active time zone must be global. You can see how a similar situation is resolved in django.utils.translation and implement the same for time zones:

import pytz
from django.utils.thread_support import currentThread

_active = {}

def default_timezone ():
"""
.

"""

from django.conf import settings
_default_timezone = pytz . timezone(settings . TIME_ZONE)
global default_timezone
default_timezone = lambda : _default_timezone
return _default_timezone

def activate (tz):
if isinstance (tz, pytz . tzinfo . BaseTzInfo):
_active[currentThread()] = tz
else :
_active[currentThread()] = pytz . timezone(tz)

def deactivate ():
global _active
if currentThread() in _active:
del _active[currentThread()]

def get_timezone ():
tz = _active . get(currentThread(), None )
if tz is not None :
return tz
return default_timezone()

def to_active (dt):
tz = get_timezone()
if dt . tzinfo is None :
dt = default_timezone() . localize(dt)
return dt . astimezone(tz)

def to_default (dt):
if dt . tzinfo is None :
return default_timezone() . localize(dt)
else :
return dt . astimezone(default_timezone())

The activate() function sets the current time zone, deactivate() returns the default belt. to_default() and to_active() convert the time to the server belt or current. It remains to write your own model field:

class TimezoneDateTimeField (models . DateTimeField):
__metaclass__ = models . SubfieldBase

def _to_python ( self , value):
"""
datetime
"""

return super (TimezoneDateTimeField, self ) . to_python(value)

def to_python ( self , value):
"""
datetime.

"""

if value is None :
return value
return timezone . to_active( self . _to_python(value))

def get_db_prep_value ( self , value):
"""

"""

if value is not None :
value = timezone . to_default( self . _to_python(value))
return connection . ops . value_to_db_datetime(value)

And set the active time zone for each request, for example, by adding TimezoneMiddleware :

class TimezoneMiddleware ( object ):
def process_request ( self , request):
...
timezone . activate(request . timezone)
return None

def process_response ( self , request, response):
timezone . deactivate()
return response

Done, just replace the standard DateTimeField with our field and time is magically transformed everywhere: in templates, in forms, and in the admin panel. Of course, there is no limit to perfection, you can implement your own form field for hanging the active time zone for the time received from the user, you can write a filter for those cases when third-party applications are used and their models are not with our fields. What I did in one project about the ski vacation , for which all this code was written.

I hope everyone who has read this far has received either practical benefits or aesthetic pleasure, which, I believe, is familiar to many who write on Django.

PS In the recently released Django 1.2, the interface of the model fields has changed, therefore, the given code for TimezoneDateTimeField will need to be completed in accordance with the update instructions

Source: https://habr.com/ru/post/94230/


All Articles