Django and time zones

    There are several everyday things that spoil our brother’s blood from time to time: cases, numerals and time zones, with a damned transition to summer / winter time. Involuntarily, you envy the Chinese who have only one time zone for the whole country, and there are no deaths in sight. It will be quite nice to once and for all deal with time zones and conversions 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. The point is small - to implement a convenient harness. The first thing that comes to mind is to write a localtime template filter and invoke it this way:

     {{ comment.time_added|localtime }} или 
     {{ comment.time_added|localtime|date:"d.m.Y" }}

    But immediately a couple of problems arise. Firstly, the filter function does not accept the context, which means that it will not be able to determine which time zone the time should lead to. 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 the 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 to the context.

    The first problem is solved by using a tag instead of a filter (it can get context), though it will not look so pretty anymore:

     {% localtime comment.time_added %} или 
     {% localtime comment.time_added as time_added %}{{ time_added|date:"d.m.Y" }}

    Particularly risky and impatient beauty lovers can use the patch , it just allows you to transfer context to the filter.

    There is still trouble with the context, you can write your context processor , or you can use the standard django.core.context_processors.request and fill its timezone property with 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 session middleware can be removed if you are not going to cache the time zone in the session. The function get_timezone()will depend on the application and may look, for example, 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:
                city = City.objects.get(pk=city_id)
                if city.timezone:
                    return city.timezone
            except City.DoesNotExist:

        # Берём значение по умолчанию из настроек
        return pytz.timezone(settings.FALLBACK_TIMEZONE)

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

    Obviously, if we want dates and times to be automatically translated into the current time zone, then we really cannot do without some magic. We get all the data from the models through their fields - excellent, converting 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 the request object; there may not be any at all. Obviously, the 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
            _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)
            return dt.astimezone(default_timezone())

    The function activate()sets the current time zone, deactivate()returns the default time zone . to_default()and to_active()convert the time to the server belt or the current one. 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, adding TimezoneMiddleware:

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

        def process_response(self, request, response):
            return response

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

    I hope everyone who has read up to this point has received either practical benefit or aesthetic pleasure, which I believe is familiar to many Django writers.

    PS In the recently released Django 1.2, the interface of the model fields has changed, so the code forTimezoneDateTimeFieldneed to finish in accordance with the update instructions

    Also popular now: