Skip to content

Commit

Permalink
Add timezone support in the admin. Hacky pytz approach for now.
Browse files Browse the repository at this point in the history
  • Loading branch information
KillaTron committed Nov 27, 2012
1 parent ecd11c4 commit 062563f
Show file tree
Hide file tree
Showing 11 changed files with 410 additions and 15 deletions.
3 changes: 3 additions & 0 deletions admin_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,6 @@
# Set this to your custom notification callback
# Useful e.g. when errors occur in the admin handler
NOTIFY_CALLBACK = None

# Default timezone for use in admin dates
TIMEZONE = 'America/Los_Angeles'
49 changes: 49 additions & 0 deletions fields.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from datetime import datetime

from google.appengine.ext import db

from . import widgets, wtforms
Expand All @@ -9,8 +11,55 @@ class DateTimeField(f.DateTimeField):
widget = widgets.DateTimeTextInput()

def __init__(self, *args, **kwargs):
kwargs['format'] = '%Y-%m-%d %H:%M:%S %Z' # output format
super(DateTimeField, self).__init__(*args, **kwargs)
self.input_format = '%Y-%m-%d %H:%M:%S'

def process_data(self, value):
if not isinstance(value, datetime):
# Non-datetime values are left alone
super(DateTimeField, self).process_data(value)
else:
# Already localized values are not localized
from . import admin_settings, utils
try:
pytz = utils.import_pytz()
except ImportError: # Fall back to default behavior if no pytz available
return super(DateTimeField, self).process_data(value)
zone = pytz.timezone(admin_settings.TIMEZONE)
if not value.tzinfo:
value = pytz.utc.localize(value)
self.data = value.astimezone(zone)

def _value(self):
try:
from . import utils
utils.import_pytz()
except ImportError: # Fall back to default behavior if no pytz available
return super(DateTimeField, self).value()
if self.raw_data:
return ' '.join(self.raw_data)
else:
data = self.data and self.data.strftime(self.format) or ''
if self.data.tzinfo:
return data.replace(self.data.tzinfo._tzname, self.data.tzinfo.zone)
return data

def process_formdata(self, valuelist):
if valuelist:
date_str = ' '.join(valuelist).strip()
from . import utils
pytz = utils.import_pytz()
date_without_tz, _, zone_name = date_str.rpartition(' ')
try:
zone = pytz.timezone(zone_name)
except pytz.UnknownTimeZoneError:
raise ValueError(self.gettext('Not a valid timezone value'))
try:
self.data = zone.localize(datetime.strptime(date_without_tz, self.input_format))
except ValueError:
self.data = None
raise ValueError(self.gettext('Not a valid datetime value'))

class DateField(f.DateField):
'''Custom DateField that use the appengine_admin DateTextInput.'''
Expand Down
3 changes: 3 additions & 0 deletions handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ def render(self, path, template_kwargs={}):
'uri_for': lambda route_name, *a, **kw: self.uri_for('appengine_admin.%s' % route_name, *a, **kw),
'get_messages': self.get_messages,
'csrf_token': self.get_csrf_token,
'settings': {
'TIMEZONE': admin_settings.TIMEZONE,
}
})
if hasattr(self, 'models'):
template_kwargs['models'] = self.models
Expand Down
4 changes: 4 additions & 0 deletions media/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ body {
width: 40%;
}

.ui_tpicker_timezone select {
width: auto;
}

.ajax_add_key_input {
width: 40%;
}
Expand Down
301 changes: 301 additions & 0 deletions media/js/jstz.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
/*jslint undef: true */
/*global console*/

/**
* Namespace to hold all the code for timezone detection.
*/
var jstz = (function () {
'use strict';
var HEMISPHERE_SOUTH = 's',

/**
* Gets the offset in minutes from UTC for a certain date.
* @param {Date} date
* @returns {Number}
*/
get_date_offset = function (date) {
var offset = -date.getTimezoneOffset();
return (offset !== null ? offset : 0);
},

get_january_offset = function () {
return get_date_offset(new Date(2010, 0, 1, 0, 0, 0, 0));
},

get_june_offset = function () {
return get_date_offset(new Date(2010, 5, 1, 0, 0, 0, 0));
},

/**
* Private method.
* Checks whether a given date is in daylight savings time.
* If the date supplied is after june, we assume that we're checking
* for southern hemisphere DST.
* @param {Date} date
* @returns {Boolean}
*/
date_is_dst = function (date) {
var base_offset = ((date.getMonth() > 5 ? get_june_offset()
: get_january_offset())),
date_offset = get_date_offset(date);

return (base_offset - date_offset) !== 0;
},

/**
* This function does some basic calculations to create information about
* the user's timezone.
*
* Returns a key that can be used to do lookups in jstz.olson.timezones.
*
* @returns {String}
*/

lookup_key = function () {
var january_offset = get_january_offset(),
june_offset = get_june_offset(),
diff = get_january_offset() - get_june_offset();

if (diff < 0) {
return january_offset + ",1";
} else if (diff > 0) {
return june_offset + ",1," + HEMISPHERE_SOUTH;
}

return january_offset + ",0";
},

/**
* Uses get_timezone_info() to formulate a key to use in the olson.timezones dictionary.
*
* Returns a primitive object on the format:
* {'timezone': TimeZone, 'key' : 'the key used to find the TimeZone object'}
*
* @returns Object
*/
determine = function () {
var key = lookup_key();
return new jstz.TimeZone(jstz.olson.timezones[key]);
};

return {
determine_timezone : function () {
if (typeof console !== 'undefined') {
console.log("jstz.determine_timezone() is deprecated and will be removed in an upcoming version. Please use jstz.determine() instead.");
}
return determine();
},
determine: determine,
date_is_dst : date_is_dst
};
}());

/**
* Simple object to perform ambiguity check and to return name of time zone.
*/
jstz.TimeZone = function (tz_name) {
'use strict';
var timezone_name = null,

name = function () {
return timezone_name;
},

/**
* Checks if a timezone has possible ambiguities. I.e timezones that are similar.
*
* For example, if the preliminary scan determines that we're in America/Denver.
* We double check here that we're really there and not in America/Mazatlan.
*
* This is done by checking known dates for when daylight savings start for different
* timezones during 2010 and 2011.
*/
ambiguity_check = function () {
var ambiguity_list = jstz.olson.ambiguity_list[timezone_name],
length = ambiguity_list.length,
i = 0,
tz = ambiguity_list[0];

for (; i < length; i += 1) {
tz = ambiguity_list[i];

if (jstz.date_is_dst(jstz.olson.dst_start_dates[tz])) {
timezone_name = tz;
return;
}
}
},

/**
* Checks if it is possible that the timezone is ambiguous.
*/
is_ambiguous = function () {
return typeof (jstz.olson.ambiguity_list[timezone_name]) !== 'undefined';
};



timezone_name = tz_name;
if (is_ambiguous()) {
ambiguity_check();
}

return {
name: name
};
};

jstz.olson = {};

/*
* The keys in this dictionary are comma separated as such:
*
* First the offset compared to UTC time in minutes.
*
* Then a flag which is 0 if the timezone does not take daylight savings into account and 1 if it
* does.
*
* Thirdly an optional 's' signifies that the timezone is in the southern hemisphere,
* only interesting for timezones with DST.
*
* The mapped arrays is used for constructing the jstz.TimeZone object from within
* jstz.determine_timezone();
*/
jstz.olson.timezones = {
'-720,0' : 'Etc/GMT+12',
'-660,0' : 'Pacific/Pago_Pago',
'-600,1' : 'America/Adak',
'-600,0' : 'Pacific/Honolulu',
'-570,0' : 'Pacific/Marquesas',
'-540,0' : 'Pacific/Gambier',
'-540,1' : 'America/Anchorage',
'-480,1' : 'America/Los_Angeles',
'-480,0' : 'Pacific/Pitcairn',
'-420,0' : 'America/Phoenix',
'-420,1' : 'America/Denver',
'-360,0' : 'America/Guatemala',
'-360,1' : 'America/Chicago',
'-360,1,s' : 'Pacific/Easter',
'-300,0' : 'America/Bogota',
'-300,1' : 'America/New_York',
'-270,0' : 'America/Caracas',
'-240,1' : 'America/Halifax',
'-240,0' : 'America/Santo_Domingo',
'-240,1,s' : 'America/Asuncion',
'-210,1' : 'America/St_Johns',
'-180,1' : 'America/Godthab',
'-180,0' : 'America/Argentina/Buenos_Aires',
'-180,1,s' : 'America/Montevideo',
'-120,0' : 'America/Noronha',
'-120,1' : 'Etc/GMT+2',
'-60,1' : 'Atlantic/Azores',
'-60,0' : 'Atlantic/Cape_Verde',
'0,0' : 'Etc/UTC',
'0,1' : 'Europe/London',
'60,1' : 'Europe/Berlin',
'60,0' : 'Africa/Lagos',
'60,1,s' : 'Africa/Windhoek',
'120,1' : 'Asia/Beirut',
'120,0' : 'Africa/Johannesburg',
'180,1' : 'Europe/Moscow',
'180,0' : 'Asia/Baghdad',
'210,1' : 'Asia/Tehran',
'240,0' : 'Asia/Dubai',
'240,1' : 'Asia/Yerevan',
'270,0' : 'Asia/Kabul',
'300,1' : 'Asia/Yekaterinburg',
'300,0' : 'Asia/Karachi',
'330,0' : 'Asia/Kolkata',
'345,0' : 'Asia/Kathmandu',
'360,0' : 'Asia/Dhaka',
'360,1' : 'Asia/Omsk',
'390,0' : 'Asia/Rangoon',
'420,1' : 'Asia/Krasnoyarsk',
'420,0' : 'Asia/Jakarta',
'480,0' : 'Asia/Shanghai',
'480,1' : 'Asia/Irkutsk',
'525,0' : 'Australia/Eucla',
'525,1,s' : 'Australia/Eucla',
'540,1' : 'Asia/Yakutsk',
'540,0' : 'Asia/Tokyo',
'570,0' : 'Australia/Darwin',
'570,1,s' : 'Australia/Adelaide',
'600,0' : 'Australia/Brisbane',
'600,1' : 'Asia/Vladivostok',
'600,1,s' : 'Australia/Sydney',
'630,1,s' : 'Australia/Lord_Howe',
'660,1' : 'Asia/Kamchatka',
'660,0' : 'Pacific/Noumea',
'690,0' : 'Pacific/Norfolk',
'720,1,s' : 'Pacific/Auckland',
'720,0' : 'Pacific/Tarawa',
'765,1,s' : 'Pacific/Chatham',
'780,0' : 'Pacific/Tongatapu',
'780,1,s' : 'Pacific/Apia',
'840,0' : 'Pacific/Kiritimati'
};


/**
* This object contains information on when daylight savings starts for
* different timezones.
*
* The list is short for a reason. Often we do not have to be very specific
* to single out the correct timezone. But when we do, this list comes in
* handy.
*
* Each value is a date denoting when daylight savings starts for that timezone.
*/
jstz.olson.dst_start_dates = {
'America/Denver' : new Date(2011, 2, 13, 3, 0, 0, 0),
'America/Mazatlan' : new Date(2011, 3, 3, 3, 0, 0, 0),
'America/Chicago' : new Date(2011, 2, 13, 3, 0, 0, 0),
'America/Mexico_City' : new Date(2011, 3, 3, 3, 0, 0, 0),
'Atlantic/Stanley' : new Date(2011, 8, 4, 7, 0, 0, 0),
'America/Asuncion' : new Date(2011, 9, 2, 3, 0, 0, 0),
'America/Santiago' : new Date(2011, 9, 9, 3, 0, 0, 0),
'America/Campo_Grande' : new Date(2011, 9, 16, 5, 0, 0, 0),
'America/Montevideo' : new Date(2011, 9, 2, 3, 0, 0, 0),
'America/Sao_Paulo' : new Date(2011, 9, 16, 5, 0, 0, 0),
'America/Los_Angeles' : new Date(2011, 2, 13, 8, 0, 0, 0),
'America/Santa_Isabel' : new Date(2011, 3, 5, 8, 0, 0, 0),
'America/Havana' : new Date(2011, 2, 13, 2, 0, 0, 0),
'America/New_York' : new Date(2011, 2, 13, 7, 0, 0, 0),
'Asia/Gaza' : new Date(2011, 2, 26, 23, 0, 0, 0),
'Asia/Beirut' : new Date(2011, 2, 27, 1, 0, 0, 0),
'Europe/Minsk' : new Date(2011, 2, 27, 2, 0, 0, 0),
'Europe/Helsinki' : new Date(2011, 2, 27, 4, 0, 0, 0),
'Europe/Istanbul' : new Date(2011, 2, 28, 5, 0, 0, 0),
'Asia/Damascus' : new Date(2011, 3, 1, 2, 0, 0, 0),
'Asia/Jerusalem' : new Date(2011, 3, 1, 6, 0, 0, 0),
'Africa/Cairo' : new Date(2010, 3, 30, 4, 0, 0, 0),
'Asia/Yerevan' : new Date(2011, 2, 27, 4, 0, 0, 0),
'Asia/Baku' : new Date(2011, 2, 27, 8, 0, 0, 0),
'Pacific/Auckland' : new Date(2011, 8, 26, 7, 0, 0, 0),
'Pacific/Fiji' : new Date(2010, 11, 29, 23, 0, 0, 0),
'America/Halifax' : new Date(2011, 2, 13, 6, 0, 0, 0),
'America/Goose_Bay' : new Date(2011, 2, 13, 2, 1, 0, 0),
'America/Miquelon' : new Date(2011, 2, 13, 5, 0, 0, 0),
'America/Godthab' : new Date(2011, 2, 27, 1, 0, 0, 0)
};

/**
* The keys in this object are timezones that we know may be ambiguous after
* a preliminary scan through the olson_tz object.
*
* The array of timezones to compare must be in the order that daylight savings
* starts for the regions.
*/
jstz.olson.ambiguity_list = {
'America/Denver' : ['America/Denver', 'America/Mazatlan'],
'America/Chicago' : ['America/Chicago', 'America/Mexico_City'],
'America/Asuncion' : ['Atlantic/Stanley', 'America/Asuncion', 'America/Santiago', 'America/Campo_Grande'],
'America/Montevideo' : ['America/Montevideo', 'America/Sao_Paulo'],
'Asia/Beirut' : ['Asia/Gaza', 'Asia/Beirut', 'Europe/Minsk', 'Europe/Helsinki', 'Europe/Istanbul', 'Asia/Damascus', 'Asia/Jerusalem', 'Africa/Cairo'],
'Asia/Yerevan' : ['Asia/Yerevan', 'Asia/Baku'],
'Pacific/Auckland' : ['Pacific/Auckland', 'Pacific/Fiji'],
'America/Los_Angeles' : ['America/Los_Angeles', 'America/Santa_Isabel'],
'America/New_York' : ['America/Havana', 'America/New_York'],
'America/Halifax' : ['America/Goose_Bay', 'America/Halifax'],
'America/Godthab' : ['America/Miquelon', 'America/Godthab']
};
Loading

0 comments on commit 062563f

Please sign in to comment.