forked from gergely/calendar-social
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:
parent
26c31bcc04
commit
8a46f3c66a
@ -1,6 +1,7 @@
|
|||||||
from flask_babelex import lazy_gettext as _
|
from flask_babelex import lazy_gettext as _
|
||||||
from flask_wtf import FlaskForm
|
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.ext.dateutil.fields import DateTimeField
|
||||||
from wtforms.validators import DataRequired, Email, ValidationError
|
from wtforms.validators import DataRequired, Email, ValidationError
|
||||||
from wtforms.widgets import TextArea
|
from wtforms.widgets import TextArea
|
||||||
@ -17,14 +18,72 @@ class RegistrationForm(FlaskForm):
|
|||||||
raise ValidationError(_('The two passwords must match!'))
|
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):
|
class EventForm(FlaskForm):
|
||||||
title = StringField(_('Title'), validators=[DataRequired()])
|
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()])
|
start_time = DateTimeField(_('Start time'), validators=[DataRequired()])
|
||||||
end_time = DateTimeField(_('End time'), validators=[DataRequired()])
|
end_time = DateTimeField(_('End time'), validators=[DataRequired()])
|
||||||
all_day = BooleanField(_('All day'))
|
all_day = BooleanField(_('All day'))
|
||||||
description = StringField(_('Description'), widget=TextArea())
|
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):
|
def validate_end_time(self, field):
|
||||||
if field.data < self.start_time.data:
|
if field.data < self.start_time.data:
|
||||||
raise ValidationError(_('End time must be later than start time!'))
|
raise ValidationError(_('End time must be later than start time!'))
|
||||||
|
@ -83,5 +83,18 @@ class Event(db.Model):
|
|||||||
#: The description of the event
|
#: The description of the event
|
||||||
description = db.Column(db.UnicodeText())
|
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):
|
def __repr__(self):
|
||||||
return f'<Event {self.id} ({self.title}) of {self.user}>'
|
return f'<Event {self.id} ({self.title}) of {self.user}>'
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
table.calendar {
|
table.calendar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr.month > td {
|
tr.month > td {
|
||||||
@ -46,14 +47,31 @@
|
|||||||
background-color: #d8d8d8;
|
background-color: #d8d8d8;
|
||||||
}
|
}
|
||||||
|
|
||||||
td > div.event {
|
tr.week > td > div.event {
|
||||||
border: 1px solid green;
|
border: 1px solid green;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.sizer > td {
|
||||||
|
width: 14.2857%;
|
||||||
|
height: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<table class="calendar">
|
<table class="calendar">
|
||||||
<thead>
|
<thead>
|
||||||
|
<tr class="sizer">
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
<tr class="month">
|
<tr class="month">
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ url_for('hello', date=calendar.prev_year) }}">« {{ calendar.prev_year_year }}</a>
|
<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>
|
<span class="day-num">{{ day.day }}</span>
|
||||||
{% for event in calendar.day_events(day, user=current_user) %}
|
{% for event in calendar.day_events(day, user=current_user) %}
|
||||||
<div class="event">
|
<div class="event">
|
||||||
|
{{ event.start_time_tz.strftime('%H:%M') }}–{{ event.end_time_tz.strftime('%H:%M') }}
|
||||||
|
({{ event.time_zone }})
|
||||||
{{ event.title }}
|
{{ event.title }}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
Loading…
Reference in New Issue
Block a user