Josh
Josh

Reputation: 13516

How to make TimeField timezone-aware?

There are occasions when you need to collect a time from a user without collecting an associated date. For example, if the user is configuring a repeating event that runs every day at the same time. Django's TimeField doesn't play with timezones though. However, in this particular case (and probably any time you record a time by itself), timezone is an important factor. So, how do you store a timezone-aware time?

Upvotes: 14

Views: 9624

Answers (3)

Josh
Josh

Reputation: 13516

The answer is you don't. For a time to be timezone aware, it has to have a date associated with it. Think of daylight savings... My solution for this was to use a DateTimeField on the model and to override the form like so:

# models.py
class MyModel(models.Model):
    time_of_day = models.DateTimeField()


# form_fields.py
from django.forms.util import from_current_timezone, to_current_timezone
from django.utils import timezone

class TzAwareTimeField(forms.fields.TimeField):
    def prepare_value(self, value):
        if isinstance(value, datetime.datetime):
            value = to_current_timezone(value).time()
        return super(TzAwareTimeField, self).prepare_value(value)

    def clean(self, value):
        value =  super(TzAwareTimeField, self).to_python(value)
        dt = to_current_timezone(timezone.now())
        return dt.replace(
            hour=value.hour, minute=value.minute,
            second=value.second, microsecond=value.microsecond)


# forms.py
class MyForm(forms.ModelForm):
    time_of_day = TzAwareTimeField()

Upvotes: 11

Ali Husham
Ali Husham

Reputation: 936

  1. in your model time_field = models.TimeField(null=True)

  2. create functions that convert the time to datetime then convert it to UTC datetime then save it. Django will automatically take the time only from the datetime.

  3. Create a function that take the time stored in the database and convert it to the current timezone

  • Note: You should activate the timezone of the current user in the middleware.
import datetime

import pytz
from django.utils import timezone


def ConvertToUTC(parsed):
    date_time = timezone.get_current_timezone().localize(parsed)
    utc_date_time = date_time.astimezone(pytz.utc)
    return utc_date_time


def ConvertToTimeZone(value, format):
    value = value.strftime(format)
    value = datetime.datetime.strptime(value, format)
    value = value.astimezone(timezone.get_current_timezone())

    return value.strftime(format)

  1. create custom field serializer that use the functions in order to make the conversions.

class TimeSer(Field):
    default_error_messages = {
        'invalid': 'Time has wrong format, expecting %H:%M:%S%z.',
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def to_internal_value(self, value):
        try:
            parsed = datetime.datetime.strptime(value, '%H:%M:%S')
        except (ValueError, TypeError) as e:
            pass
        else:
            return ConvertToUTC(parsed)
        self.fail('invalid')

    def to_representation(self, value):

        if not value:
            return None

        if isinstance(value, str):
            return value
        if isinstance(value, datetime.time):
            return ConvertToTimeZone(value,'%H:%M:%S')

        return None

Upvotes: 0

Tim Tisdall
Tim Tisdall

Reputation: 10382

This is untested and incomplete:

class TimeFieldWithZone(TimeField):
    def db_type(self, connection):
        if (connection.settings_dict['ENGINE'] == 
            'django.db.backends.postgresql_psycopg2'):
            return 'time with time zone'
        raise Exception('Unsupported database type')

    def get_db_prep_value(self, value, *args, **kwargs):
        try:
            return super(TimeFieldWithZone, self).get_db_prep_value(
                value, *args, **kwargs)
        except ValueError:
            return six.text_type(value)

This will use Postgres' time with time zone datatype. It will break if you pass it a string in the format 'HH:MM:SS.mmmmmm+HH:MM' and using auto_now will try to save a naive time (not sure if that throws an error).

edit

In the generic backend code an exception is thrown if you try inserting a time with a timezone other than UTC.

edit 2

I added an updated get_db_prep_value to convert a provided time with timezone into a string, but it only works if the provided timezone outputs a utc offset (which may be ambiguous without a date).

It seems time with time zone is a little misleading... As far as I can tell, it actually stores a time with a UTC offset and NOT a timezone. So it'd be difficult to take the returned value, add a calendar date, and get back the proper time with regard to daylight savings time.

Upvotes: 1

Related Questions