# Calendar.social # Copyright (C) 2018 Gergely Polonkai # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . """Forms for Calendar.social """ from flask_babelex import lazy_gettext as _ from flask_security.forms import LoginForm as BaseLoginForm from flask_wtf import FlaskForm 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 class RegistrationForm(FlaskForm): """Registration form """ username = StringField(_('Username'), validators=[DataRequired()]) email = StringField(_('Email address'), validators=[Email()]) password = PasswordField(_('Password'), validators=[DataRequired()]) password_retype = PasswordField(_('Password, once more'), validators=[DataRequired()]) def validate_password_retype(self, field): """Validate the ``password_retype`` field Its value must match the ``password`` field’s. """ if field.data != self.password.data: raise ValidationError(_('The two passwords must match!')) class TimezoneField(SelectField): """Field for selecting a time zone Note: this field overrides whatever is passed to the ``choices`` parameter, and fills ``choices`` with the common timezones of pytz. In every other aspects, it behaves exactly like `SelectField`. """ def __init__(self, *args, **kwargs): self.data = None 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): """Check if ``value`` is a valid pytz time zone instance """ 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 self.is_pytz_instance(value): self.data = value return try: self.data = pytz.timezone(value) except pytz.exceptions.UnknownTimeZoneError: 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): """Form for event creation/editing """ title = StringField(_('Title'), 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): """Populate ``obj`` with event data """ FlaskForm.populate_obj(self, obj) timezone = self.time_zone.data obj.time_zone = str(timezone) obj.start_time = timezone.localize(self.start_time.data).astimezone(pytz.utc) obj.end_time = timezone.localize(self.end_time.data).astimezone(pytz.utc) def validate_end_time(self, field): """Validate the ``end_time`` field Its value must be later than the value of the ``start_time`` field. """ if field.data < self.start_time.data: raise ValidationError(_('End time must be later than start time!')) class SettingsForm(FlaskForm): """Form for user settings """ timezone = TimezoneField(_('Time zone'), validators=[DataRequired()]) def __init__(self, user, *args, **kwargs): self.user = user kwargs.setdefault('timezone', user.timezone) FlaskForm.__init__(self, *args, **kwargs) def populate_obj(self, user): """Populate ``obj`` with event data """ for name, field in self._fields.items(): if not (hasattr(self.__class__, name) and not hasattr(FlaskForm, name)): continue user.settings[name] = str(field.data) class LoginForm(BaseLoginForm): """Login form for Calendar.social """ email = StringField(_('Username or email'), validators=[DataRequired()]) def __init__(self, *args, **kwargs): super(LoginForm, self).__init__(*args, **kwargs) self.user = None def validate(self): from flask_security.utils import _datastore from flask_security.utils import verify_and_update_password from .models import AuditLog ret = super(LoginForm, self).validate() if self.user is None: self.user = _datastore.get_user(self.email.data) if self.user is None: # We can’t figure out the user that’s trying to log in, just return return ret if not verify_and_update_password(self.password.data, self.user): AuditLog.log(self.user, AuditLog.TYPE_LOGIN_FAIL) return ret