# 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, StopValidation, ValidationError from wtforms.widgets import TextArea class UsernameAvailable(object): # pylint: disable=too-few-public-methods """Checks if a username is available """ def __init__(self, message=None): self.message = message def __call__(self, form, field): from sqlalchemy.orm.exc import NoResultFound from calsocial.models import User # If there is no data, we don’t raise an error; it’s not the task of this validator to # check the validity of the username if not field.data: return try: User.query.filter(User.username == field.data).one() except NoResultFound: return if self.message is None: message = field.gettext('This username is not available') else: message = self.message field.errors[:] = [] raise StopValidation(message) class EmailAvailable(object): # pylint: disable=too-few-public-methods """Checks if an email address is available """ def __init__(self, message=None): self.message = message def __call__(self, form, field): from sqlalchemy.orm.exc import NoResultFound from calsocial.models import User # If there is no data, we don’t raise an error; it’s not the task of this validator to # check the validity of the username if not field.data: return try: User.query.filter(User.email == field.data).one() except NoResultFound: return if self.message is None: message = field.gettext('This email address can not be used') else: message = self.message field.errors[:] = [] raise StopValidation(message) class RegistrationForm(FlaskForm): """Registration form """ username = StringField(_('Username'), validators=[DataRequired(), UsernameAvailable()]) email = StringField(_('Email address'), validators=[Email(), EmailAvailable()]) 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 class ProfileField(StringField): """Input field for profiles """ def process_formdata(self, valuelist): from sqlalchemy.orm.exc import NoResultFound from .models import User, Profile if not valuelist: self.data = None return value = valuelist[0] if value.startswith('@'): value = value[1:] try: self.data = Profile.query.join(User).filter(User.username == value).one() except NoResultFound: self.data = None raise ValueError('Unknown user') def _value(self): if self.data: return self.data.fqn return '' class InviteForm(FlaskForm): """Form for event invitations """ invitee = ProfileField(validators=[DataRequired()]) def __init__(self, event, *args, **kwargs): FlaskForm.__init__(self, *args, **kwargs) self.event = event def validate_invitee(self, field): """Validate the value of the invitee field :raises ValidationError: If the given user is already invited """ from sqlalchemy.orm.exc import NoResultFound from .models import Invitation try: Invitation.query \ .filter(Invitation.event == self.event) \ .filter(Invitation.invitee == field.data) \ .one() raise ValidationError(_('User is already invited')) except NoResultFound: pass