Time zones used in SchoolTool

In schooltool, there are three significant time zones: the school timezone (the application preference), the user’s preferred one, and UTC. Practically, the user’s preferred timezone will differ from the server timezone only in special cases, but we need to handle this difference anyway.

The user’s preferred timezone should only be used when accepting datetime input from the user, and when determining the day boundaries on the calendar views.

A rule of thumb is that all the datetimes in the system should use the application timezone. Business processes such as timetabling, attendance and so on use the application timezone. The school day boundaries are determined in this timezone.

As a historical legacy, SchoolTool internally stores most times in UTC, and some parts of SchoolTool might still require that the timezone is used in calendar events’ dtstart attributes.

The application timezone can be acquired thus:

>>> import pytz
>>> from schooltool.app.interfaces import ISchoolToolApplication
>>> from schooltool.app.interfaces import IApplicationPreferences
>>> app = ISchoolToolApplication(None)
>>> apptimezone = pytz.timezone(IApplicationPreferences(app).timezone)

In order to get the user preferred timezone, you need the browser request:

>>> from schooltool.app.browser import ViewPreferences
>>> usertz = ViewPreferences(request).timezone

Converting timezones

There are two kinds of timezone conversion: you might have a naïve datetime and want to apply a certain timezone for it, of you might want to express a timezone-aware datetime in another timezone.

For the latter, use astimezone:

>>> tz_aware_dt.astimezone(other_tz)

For the former, that is making the datetime timezone-aware, always use timezone.localize:

>>> tz_aware_dt = timezone.localize(naive_dt)

Never use datetime.replace(tzinfo=timezone) as it will get you incorrect results most of the time. For example:

### DON'T DO THIS! This is wrong. ###
>>> import pytz
>>> import datetime
>>> vilnius = pytz.timezone('Europe/Vilnius')
>>> datetime.datetime(2006, 1, 1, 0, 0, tzinfo=vilnius)
datetime.datetime(2006, 1, 1, 0, 0, tzinfo=<DstTzInfo 'Europe/Vilnius' WMT+1:24:00 STD>)

This gets the timezone that was used in Vilnius in 1900! Even if the timezone is unchanged since the start of the ZoneInfo database, we loose all hope of getting the daylight savings right. If we use the localize method we get the correct result:

>>> vilnius.localize(datetime.datetime(2006, 1, 1, 0, 0))
datetime.datetime(2006, 1, 1, 0, 0, tzinfo=<DstTzInfo 'Europe/Vilnius' EET+2:00:00 STD>)

Let’s look at the UTC expressions of both times to see the difference clearly:

>>> good = vilnius.localize(datetime.datetime(2006, 1, 1, 0, 0))
>>> wrong = datetime.datetime(2006, 1, 1, 0, 0, tzinfo=vno)
>>> good.astimezone(pytz.utc)
datetime.datetime(2005, 12, 31, 22, 0, tzinfo=<UTC>)
>>> wrong.astimezone(pytz.utc)
datetime.datetime(2005, 12, 31, 22, 36, tzinfo=<UTC>)

When using the datetime.replace for replacing other datetime components, you should convert the result to the desired timezone, as your manipulation might have shifted the datetime into a different timezones because the daylight savings have changed.

On the day when the daylight savings time switches, timezone.localize might raise if you pass the “missing hour”. In this example, the time is moved from 2:59AM to 4:00AM, so 3:00AM does not exist:

>>> vilnius.localize(datetime.datetime(2006, 3, 26, 3, 1))
Traceback (most recent call last):
  ...
IndexError: list index out of range

What is Today?

This question arises when displaying timetable and calendar events on the calendar of the user. When the timezones of the application and user preference differ, actually two schooldays should be filtered for display.

Never use datetime.date.today(). You’ll get a date in the system timezone, that might be terribly wrong if the application timezone and the user’s preferred timezone differ from that.

If you need a date for today to be displayed for the user do this:

>>> from datetime import datetime
>>> from pytz import utc, timezone
>>> def today(self, tz):
...     current_time = utc.localize(datetime.utcnow())
...     return current_time.astimezone(tz).date()

Here’s an example of how the correct date usage can be tested. These two timezones are on the different sides of the International Date Line, so the dates in these timezones are always different:

>>> from schooltool.app.browser.cal import CalendarViewBase
>>> view = CalendarViewBase(None, TestRequest())

>>> current_time = utc.localize(datetime.utcnow())

>>> tz = timezone('Etc/GMT+12')
>>> today_date = current_time.astimezone(tz).date()
>>> view.timezone = tz
>>> view.today() == today_date
True

>>> tz = timezone('Etc/GMT-12')
>>> today_date = current_time.astimezone(tz).date()
>>> view.timezone = tz
>>> view.today() == today_date
True

If you want to store a date of an event - store:

>>> utc.localize(datetime.datetime.utcnow())

and display only the date part to the user, using:

>>> dt.astimezone(user_tz).date()

Table Of Contents

Previous topic

SchoolTool calendaring library

Next topic

Resource booking in SchoolTool

This Page