Add proper time zone support

Until now event timestamps were saved in the server’s time zone.  Now they are saved in UTC,
considering the time zone set by the creator of the event.
This commit is contained in:
Gergely Polonkai 2018-07-02 15:07:53 +02:00
parent 26c31bcc04
commit 8a46f3c66a
3 changed files with 95 additions and 3 deletions

View File

@ -1,6 +1,7 @@
from flask_babelex import lazy_gettext as _
from flask_wtf import FlaskForm
from wtforms import PasswordField, StringField, BooleanField
import pytz
from wtforms import BooleanField, PasswordField, SelectField, StringField
from wtforms.ext.dateutil.fields import DateTimeField
from wtforms.validators import DataRequired, Email, ValidationError
from wtforms.widgets import TextArea
@ -17,14 +18,72 @@ class RegistrationForm(FlaskForm):
raise ValidationError(_('The two passwords must match!'))
class TimezoneField(SelectField):
def __init__(self, *args, **kwargs):
kwargs.update({
'choices': [
(pytz.timezone(tz), tz.replace('_', ' '))
for tz in pytz.common_timezones
],
})
SelectField.__init__(self, *args, **kwargs)
def process_formdata(self, valuelist):
if not valuelist:
self.data = None
return
try:
self.data = pytz.timezone(valuelist[0])
except pytz.exceptions.UnknownTimeZoneError:
self.data = None
raise ValueError('Unknown time zone')
@staticmethod
def is_pytz_instance(value):
return value is pytz.UTC or isinstance(value, pytz.tzinfo.BaseTzInfo)
def process_data(self, value):
if value is None:
self.data = None
return
if is_pytz_instance(value):
self.data = value
return
try:
self.data = pytz.timezone(value)
except Exception as exc:
raise ValueError(f'Unknown time zone {value}')
def iter_choices(self):
for value, label in self.choices:
yield (value, label, value == self.data)
class EventForm(FlaskForm):
title = StringField(_('Title'), validators=[DataRequired()])
time_zone = StringField(_('Time zone'), validators=[DataRequired()])
time_zone = TimezoneField(_('Time zone'), validators=[DataRequired()])
start_time = DateTimeField(_('Start time'), validators=[DataRequired()])
end_time = DateTimeField(_('End time'), validators=[DataRequired()])
all_day = BooleanField(_('All day'))
description = StringField(_('Description'), widget=TextArea())
def populate_obj(self, obj):
FlaskForm.populate_obj(self, obj)
tz = self.time_zone.data
obj.time_zone = str(tz)
obj.start_time = tz.localize(self.start_time.data).astimezone(pytz.utc)
obj.end_time = tz.localize(self.end_time.data).astimezone(pytz.utc)
def validate_end_time(self, field):
if field.data < self.start_time.data:
raise ValidationError(_('End time must be later than start time!'))

View File

@ -83,5 +83,18 @@ class Event(db.Model):
#: The description of the event
description = db.Column(db.UnicodeText())
def __as_tz(self, timestamp):
from pytz import timezone, utc
return utc.localize(timestamp).astimezone(timezone(self.time_zone))
@property
def start_time_tz(self):
return self.__as_tz(self.start_time)
@property
def end_time_tz(self):
return self.__as_tz(self.end_time)
def __repr__(self):
return f'<Event {self.id} ({self.title}) of {self.user}>'

View File

@ -6,6 +6,7 @@
table.calendar {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
tr.month > td {
@ -46,14 +47,31 @@
background-color: #d8d8d8;
}
td > div.event {
tr.week > td > div.event {
border: 1px solid green;
background-color: white;
border-radius: 2px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
tr.sizer > td {
width: 14.2857%;
height: 0;
}
</style>
<table class="calendar">
<thead>
<tr class="sizer">
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr class="month">
<td>
<a href="{{ url_for('hello', date=calendar.prev_year) }}">« {{ calendar.prev_year_year }}</a>
@ -101,6 +119,8 @@
<span class="day-num">{{ day.day }}</span>
{% for event in calendar.day_events(day, user=current_user) %}
<div class="event">
{{ event.start_time_tz.strftime('%H:%M') }}{{ event.end_time_tz.strftime('%H:%M') }}
({{ event.time_zone }})
{{ event.title }}
</div>
{% endfor %}