diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..18b16248 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "wtforms_widgets"] + path = wtforms_widgets + url = https://github.com/agdsn/wtforms-widgets.git + branch = pycroft diff --git a/build/Dockerfile b/build/Dockerfile index c9987f3e..b4a8cf9e 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -23,6 +23,7 @@ RUN apt-get update && apt-get install -y --force-yes --no-install-recommends \ WORKDIR /opt/sipa COPY --chown=sipa:sipa ./build /opt/sipa/build/ +COPY --chown=sipa:sipa ./wtforms_widgets /opt/sipa/wtforms_widgets ARG additional_requirements RUN ./build/install_requirements.py $additional_requirements diff --git a/build/requirements/requirements.txt b/build/requirements/requirements.txt index 43d52113..5c45f0d7 100644 --- a/build/requirements/requirements.txt +++ b/build/requirements/requirements.txt @@ -24,3 +24,4 @@ recurring_ical_events~=1.0.2b0 cachetools~=5.2.0 python-dotenv~=0.21.0 pydantic~=2.4.2 +-e wtforms_widgets diff --git a/sipa/blueprints/generic.py b/sipa/blueprints/generic.py index ee52faa2..94843aed 100644 --- a/sipa/blueprints/generic.py +++ b/sipa/blueprints/generic.py @@ -18,8 +18,13 @@ from sqlalchemy.exc import DatabaseError from sipa.backends.exceptions import BackendError -from sipa.forms import flash_formerrors, LoginForm, AnonymousContactForm, \ - OfficialContactForm, PasswordRequestResetForm, PasswordResetForm +from sipa.forms import ( + LoginForm, + AnonymousContactForm, + OfficialContactForm, + PasswordRequestResetForm, + PasswordResetForm, +) from sipa.mail import send_official_contact_mail, send_contact_mail from sipa.backends.extension import backends from sipa.model import pycroft @@ -154,8 +159,6 @@ def login(): logger.info('Authentication successful', extra={'tags': {'user': username}}) flash(gettext("Anmeldung erfolgreich!"), "success") - elif form.is_submitted(): - flash_formerrors(form) if current_user.is_authenticated: # `url_redirect` would not be bad here because this would allow for URL @@ -198,8 +201,6 @@ def request_password_reset(): "Falls du die Nachricht nicht erhälst, wende dich an den Support."), "success") return redirect(url_for('.login')) - elif form.is_submitted(): - flash_formerrors(form) return render_template('generic_form.html', page_title=gettext("Passwort zurücksetzen"), form_args={'form': form, 'cancel_to': url_for('.login')}) @@ -222,8 +223,6 @@ def reset_password(token): flash(gettext("Dein Passwort wurde geändert."), "success") return redirect(url_for('.login')) - elif form.is_submitted(): - flash_formerrors(form) return render_template('generic_form.html', page_title=gettext("Passwort zurücksetzen"), form_args={'form': form, 'cancel_to': url_for('.login')}) @@ -330,8 +329,6 @@ def contact(): flash(gettext("Es gab einen Fehler beim Versenden der Nachricht."), 'error') return redirect(url_for('.index')) - elif form.is_submitted(): - flash_formerrors(form) return render_template('anonymous_contact.html', form=form) @@ -354,8 +351,6 @@ def contact_official(): flash(gettext("Es gab einen Fehler beim Versenden der Nachricht."), 'error') return redirect(url_for('.index')) - elif form.is_submitted(): - flash_formerrors(form) return render_template( 'official_contact.html', diff --git a/sipa/blueprints/register.py b/sipa/blueprints/register.py index 6ec6d340..c58bb550 100644 --- a/sipa/blueprints/register.py +++ b/sipa/blueprints/register.py @@ -13,7 +13,7 @@ from werkzeug.local import LocalProxy from sipa.backends.extension import backends, _dorm_summary -from sipa.forms import flash_formerrors, RegisterIdentifyForm, RegisterRoomForm, RegisterFinishForm +from sipa.forms import RegisterIdentifyForm, RegisterRoomForm, RegisterFinishForm from sipa.model.pycroft.api import PycroftApi, PycroftApiError from sipa.model.pycroft.exc import PycroftBackendError from sipa.utils import parse_date @@ -167,9 +167,6 @@ def identify(reg_state: RegisterState): except PycroftBackendError as e: handle_backend_error(e) - elif form.is_submitted(): - flash_formerrors(form) - return render_template('register/identify.html', title=gettext('Identifizierung'), form=form, skip_verification=suggest_skip) @@ -181,9 +178,7 @@ def room(reg_state: RegisterState): if form.validate_on_submit(): reg_state.room_confirmed = 'wrong_room' not in request.form return goto_step('data') - elif form.is_submitted(): - flash_formerrors(form) - else: + elif not form.is_submitted(): form.building.data = reg_state.building form.room.data = reg_state.room form.move_in_date.data = reg_state.move_in_date @@ -236,9 +231,7 @@ def data(reg_state: RegisterState): except PycroftBackendError as e: handle_backend_error(e) - elif form.is_submitted(): - flash_formerrors(form) - else: + elif not form.is_submitted(): form.member_begin_date.data = max(reg_state.move_in_date, date.today()) \ if reg_state.move_in_date is not None else date.today() @@ -249,13 +242,9 @@ def data(reg_state: RegisterState): "Dadurch kann dein Antrag nicht automatisch bearbeitet werden. " "Eine manuelle Bearbeitung kann mehrere Tage dauern."), 'warning') - return render_template('register/data.html', title=gettext('Konto erstellen'), form=form, - links={ - 'constitution': '../pages/legal/agdsn_constitution', - 'fee_regulation': '../pages/legal/membership_fee_regulations', - 'network_constitution': '../pages/legal/network_constitution', - 'privacy_policy': '../pages/legal/agdsn_dataprotection', - }) + return render_template( + "register/data.html", title=gettext("Konto erstellen"), form=form + ) @bp_register.route("/finish") diff --git a/sipa/blueprints/usersuite.py b/sipa/blueprints/usersuite.py index be6a58e5..bf6ec055 100644 --- a/sipa/blueprints/usersuite.py +++ b/sipa/blueprints/usersuite.py @@ -22,10 +22,18 @@ from flask_wtf import FlaskForm from markupsafe import Markup -from sipa.forms import ContactForm, ChangeMACForm, ChangeMailForm, \ - ChangePasswordForm, flash_formerrors, HostingForm, \ - PaymentForm, ActivateNetworkAccessForm, TerminateMembershipForm, \ - TerminateMembershipConfirmForm, ContinueMembershipForm +from sipa.forms import ( + ContactForm, + ChangeMACForm, + ChangeMailForm, + ChangePasswordForm, + HostingForm, + PaymentForm, + ActivateNetworkAccessForm, + TerminateMembershipForm, + TerminateMembershipConfirmForm, + ContinueMembershipForm, +) from sipa.mail import send_usersuite_contact_mail from sipa.model.fancy_property import ActiveProperty from sipa.utils import password_changeable, subscribe_to_status_page @@ -109,7 +117,6 @@ def index(): months = payment_form.months.data else: months = payment_form.months.default - flash_formerrors(payment_form) datasource = current_user.datasource context = dict(rows=rows, @@ -168,8 +175,6 @@ def contact(): .format(current_user.datasource.support_mail), 'error') return redirect(url_for('.index')) - elif form.is_submitted(): - flash_formerrors(form) form.email.default = current_user.mail.raw_value @@ -276,8 +281,6 @@ def change_password(): else: flash(gettext("Passwort wurde geändert"), "success") return redirect(url_for('.index')) - elif form.is_submitted(): - flash_formerrors(form) return render_template("generic_form.html", page_title=gettext("Passwort ändern"), form_args={'form': form, 'reset_button': True, 'cancel_to': url_for('.index')}) @@ -309,9 +312,7 @@ def change_mail(): else: flash(gettext("E-Mail-Adresse wurde geändert"), "success") return redirect(url_for('.index')) - elif form.is_submitted(): - flash_formerrors(form) - else: + elif not form.is_submitted(): form.email.data = current_user.mail.raw_value form.forwarded.data = current_user.mail_forwarded.raw_value @@ -338,8 +339,6 @@ def resend_confirm_mail(): flash(gettext('Versenden der Bestätigungs-E-Mail ist fehlgeschlagen!'), 'error') return redirect(url_for('.index')) - elif form.is_submitted(): - flash_formerrors(form) form_args = { 'form': form, @@ -384,9 +383,6 @@ def change_mac(): return redirect(url_for('.index')) - elif form.is_submitted(): - flash_formerrors(form) - form.mac.default = current_user.mac.value return render_template('usersuite/change_mac.html', @@ -428,9 +424,6 @@ def activate_network_access(): return redirect(url_for('.index')) - elif form.is_submitted(): - flash_formerrors(form) - return render_template('generic_form.html', page_title=gettext("Netzwerkanschluss aktivieren"), form_args={'form': form, 'cancel_to': url_for('.index')}) @@ -457,8 +450,6 @@ def hosting(action=None): flash(gettext("Deine Datenbank wurde erstellt."), 'success') else: current_user.userdb.change_password(form.password.data) - elif form.is_submitted(): - flash_formerrors(form) try: user_has_db = current_user.userdb.has_db @@ -495,8 +486,6 @@ def terminate_membership(): return redirect(url_for('.terminate_membership_confirm', end_date=end_date)) - elif form.is_submitted(): - flash_formerrors(form) form_args = { 'form': form, @@ -553,8 +542,6 @@ def terminate_membership_confirm(): flash(gettext("Deine Mitgliedschaft wird zum angegebenen Datum beendet."), 'success') return redirect(url_for('.index')) - elif form.is_submitted(): - flash_formerrors(form) form_args = { 'form': form, @@ -595,8 +582,6 @@ def continue_membership(): flash(gettext("Deine Mitgliedschaft wird fortgesetzt."), 'success') return redirect(url_for('.index')) - elif form.is_submitted(): - flash_formerrors(form) form_args = { 'form': form, @@ -631,8 +616,6 @@ def reset_wifi_password(): flash(Markup("{}:
{}".format(gettext("Es wurde ein neues WLAN Passwort generiert"), new_password)), 'success') return redirect(url_for('.index')) - elif form.is_submitted(): - flash_formerrors(form) form_args = { 'form': form, diff --git a/sipa/forms.py b/sipa/forms.py index fb775dc5..d615f084 100644 --- a/sipa/forms.py +++ b/sipa/forms.py @@ -1,13 +1,15 @@ import re +import typing as t from datetime import date +from dataclasses import dataclass +from functools import partial from operator import itemgetter from flask_babel import gettext, lazy_gettext -from flask import flash from flask_login import current_user -from flask_wtf import FlaskForm +from wtforms_widgets.base_form import BaseForm as FlaskForm from werkzeug.local import LocalProxy -from wtforms import ( +from wtforms_widgets.fields.core import ( BooleanField, HiddenField, PasswordField, @@ -40,6 +42,38 @@ def __init__(self, message=None): super().__init__(mac_regex, message=message) +_LINK_PLACEHOLDER = re.compile(r"\[(?P