Compare commits

...

2 Commits

Author SHA1 Message Date
Jonas Schäfer
ad229d6700 Use standard error rendering for the login form
This provides a consistent UX.
2021-03-20 16:30:42 +01:00
Jonas Schäfer
b822000f2e Improve install button layout on narrow screens
This allows the button container to add line breaks between the
buttons when necessary.
2021-03-20 16:30:42 +01:00
8 changed files with 148 additions and 126 deletions

View File

@@ -19,12 +19,11 @@ from quart import (
abort,
flash,
)
import flask_wtf
from flask_babel import lazy_gettext as _l, _
from . import prosodyclient
from .infra import client, circle_name
from .infra import client, circle_name, BaseForm
bp = Blueprint("admin", __name__, url_prefix="/admin")
@@ -35,7 +34,7 @@ async def index() -> str:
return await render_template("admin_home.html")
class PasswordResetLinkPost(flask_wtf.FlaskForm): # type: ignore
class PasswordResetLinkPost(BaseForm):
action_create = wtforms.StringField()
action_revoke = wtforms.StringField()
@@ -55,7 +54,7 @@ async def users() -> str:
)
class DeleteUserForm(flask_wtf.FlaskForm): # type:ignore
class DeleteUserForm(BaseForm):
action_delete = wtforms.SubmitField(
_l("Delete user permanently")
)
@@ -132,11 +131,11 @@ async def create_password_reset_link() -> typing.Union[str, quart.Response]:
)
class InvitesListForm(flask_wtf.FlaskForm): # type:ignore
class InvitesListForm(BaseForm):
action_revoke = wtforms.StringField()
class InvitePost(flask_wtf.FlaskForm): # type:ignore
class InvitePost(BaseForm):
circles = wtforms.SelectMultipleField(
_l("Invite to circle"),
# NOTE: This is for when/if we ever support multi-group invites.
@@ -230,7 +229,7 @@ async def invitations() -> typing.Union[str, quart.Response]:
)
class InviteForm(flask_wtf.FlaskForm): # type:ignore
class InviteForm(BaseForm):
action_revoke = wtforms.SubmitField(
_l("Revoke")
)
@@ -302,7 +301,7 @@ async def edit_invite(id_: str) -> typing.Union[str, quart.Response]:
)
class CirclePost(flask_wtf.FlaskForm): # type:ignore
class CirclePost(BaseForm):
name = wtforms.StringField(
_l("Name"),
validators=[wtforms.validators.InputRequired()],
@@ -350,7 +349,7 @@ async def create_circle() -> typing.Union[str, quart.Response]:
)
class EditCircleForm(flask_wtf.FlaskForm): # type:ignore
class EditCircleForm(BaseForm):
name = wtforms.StringField(
_l("Name"),
validators=[wtforms.validators.InputRequired()],

View File

@@ -10,6 +10,7 @@ from quart import (
)
import flask_babel
import flask_wtf
from flask_babel import _
from . import prosodyclient
@@ -55,3 +56,14 @@ def generate_error_id() -> str:
return base64.b32encode(secrets.token_bytes(8)).decode(
"ascii"
).rstrip("=")
class BaseForm(flask_wtf.FlaskForm): # type:ignore
def __init__(self, *args: typing.Any, **kwargs: typing.Any):
meta = kwargs["meta"] = dict(kwargs.get("meta", {}))
if "locales" not in meta:
locale = flask_babel.get_locale()
if locale:
meta["locales"] = [str(locale)]
super().__init__(*args, **kwargs)

View File

@@ -16,10 +16,9 @@ from quart import (
import wtforms
import flask_wtf
from flask_babel import lazy_gettext as _l
from .infra import client, selected_locale
from .infra import client, selected_locale, BaseForm
bp = Blueprint("invite", __name__)
@@ -102,7 +101,7 @@ async def view(id_: str) -> typing.Union[quart.Response,
)
class RegisterForm(flask_wtf.FlaskForm): # type:ignore
class RegisterForm(BaseForm):
localpart = wtforms.StringField(
_l("Username"),
)
@@ -148,15 +147,15 @@ async def register(id_: str) -> typing.Union[str, quart.Response]:
except aiohttp.ClientResponseError as exc:
if exc.status == 409:
form.localpart.errors.append(
_l("That username is already taken")
_l("That username is already taken.")
)
elif exc.status == 403:
form.localpart.errors.append(
_l("Registration was declined for unknown reasons")
_l("Registration was declined for unknown reasons.")
)
elif exc.status == 400:
form.localpart.errors.append(
_l("The username is not valid")
_l("The username is not valid.")
)
elif exc.status == 404:
return redirect(url_for(".view", id_=id_))
@@ -173,7 +172,7 @@ async def register(id_: str) -> typing.Union[str, quart.Response]:
)
class ResetForm(flask_wtf.FlaskForm): # type:ignore
class ResetForm(BaseForm):
password = wtforms.PasswordField(
_l("Password"),
)
@@ -216,7 +215,7 @@ async def reset(id_: str) -> typing.Union[str, quart.Response]:
except aiohttp.ClientResponseError as exc:
if exc.status == 403:
form.localpart.errors.append(
_l("Registration was declined for unknown reasons")
_l("Registration was declined for unknown reasons.")
)
elif exc.status == 404:
return redirect(url_for(".view", id_=id_))

View File

@@ -22,17 +22,16 @@ import babel
import wtforms
import flask_wtf
from flask_babel import lazy_gettext as _l, _
from . import xmpputil, _version
from .infra import client
from .infra import client, BaseForm
bp = quart.Blueprint("main", __name__)
class LoginForm(flask_wtf.FlaskForm): # type:ignore
class LoginForm(BaseForm):
address = wtforms.TextField(
_l("Address"),
validators=[wtforms.validators.InputRequired()],

View File

@@ -80,7 +80,7 @@
<div class="box warning">{#- -#}
<header>{% trans %}Invalid input{% endtrans %}</header>
{%- if error_list | length == 1 -%}
<p>{{ error_list[0] }}.</p>
<p>{{ error_list[0] }}</p>
{%- else -%}
<ul>
{%- for error in error_list -%}

View File

@@ -1,5 +1,5 @@
{% extends "base.html" %}
{% from "library.j2" import box, form_button %}
{% from "library.j2" import box, form_button, render_errors %}
{% set body_id = "login" %}
{% block head_lead %}
<title>{{ _("Snikket Login") }}</title>
@@ -14,11 +14,7 @@
<p class="form-desc">{{ _("Enter your Snikket address and password to manage your account.") }}</p>
<form method="POST" action="{{ url_for('.login') }}" name="login" id="login-form" onsubmit="return domainCheck();" data-addressid="{{ form.address.id }}" data-domain="{{ config["SNIKKET_DOMAIN"] }}">
{{ form.csrf_token }}
{% if form.errors %}
{% call box("alert", _("Login failed")) %}
<p>{{ form.errors.values() | flatten | join(", ")}}</p>
{% endcall %}
{% endif %}
{% call render_errors(form) %}{% endcall %}
<div class="box alert" role="alert" style="display: none;" id="id-warning">
<header>{% trans %}Incorrect address{% endtrans %}</header>
<p>{% trans snikket_domain=config["SNIKKET_DOMAIN"] %}This Snikket service only hosts addresses ending in <em>@{{ snikket_domain }}</em>. Your password was not sent.{% endtrans %}</p>

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2021-03-20 16:15+0100\n"
"POT-Creation-Date: 2021-03-20 16:27+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,135 +17,135 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.9.0\n"
#: snikket_web/admin.py:60
#: snikket_web/admin.py:59
msgid "Delete user permanently"
msgstr ""
#: snikket_web/admin.py:73
#: snikket_web/admin.py:72
msgid "User deleted"
msgstr ""
#: snikket_web/admin.py:116
#: snikket_web/admin.py:115
msgid "Password reset link created"
msgstr ""
#: snikket_web/admin.py:122
#: snikket_web/admin.py:121
msgid "Password reset link deleted"
msgstr ""
#: snikket_web/admin.py:141
#: snikket_web/admin.py:140
msgid "Invite to circle"
msgstr ""
#: snikket_web/admin.py:147
#: snikket_web/admin.py:146
msgid "At least one circle must be selected"
msgstr ""
#: snikket_web/admin.py:152
#: snikket_web/admin.py:151
msgid "Valid for"
msgstr ""
#: snikket_web/admin.py:154
#: snikket_web/admin.py:153
msgid "One hour"
msgstr ""
#: snikket_web/admin.py:155
#: snikket_web/admin.py:154
msgid "Twelve hours"
msgstr ""
#: snikket_web/admin.py:156
#: snikket_web/admin.py:155
msgid "One day"
msgstr ""
#: snikket_web/admin.py:157
#: snikket_web/admin.py:156
msgid "One week"
msgstr ""
#: snikket_web/admin.py:158
#: snikket_web/admin.py:157
msgid "Four weeks"
msgstr ""
#: snikket_web/admin.py:164 snikket_web/templates/admin_edit_invite.html:17
#: snikket_web/admin.py:163 snikket_web/templates/admin_edit_invite.html:17
msgid "Invitation type"
msgstr ""
#: snikket_web/admin.py:166 snikket_web/templates/library.j2:116
#: snikket_web/admin.py:165 snikket_web/templates/library.j2:116
msgid "Individual"
msgstr ""
#: snikket_web/admin.py:167 snikket_web/templates/library.j2:114
#: snikket_web/admin.py:166 snikket_web/templates/library.j2:114
msgid "Group"
msgstr ""
#: snikket_web/admin.py:173
#: snikket_web/admin.py:172
msgid "New invitation link"
msgstr ""
#: snikket_web/admin.py:235
#: snikket_web/admin.py:234
msgid "Revoke"
msgstr ""
#: snikket_web/admin.py:259
#: snikket_web/admin.py:258
msgid "Invitation created"
msgstr ""
#: snikket_web/admin.py:275
#: snikket_web/admin.py:274
msgid "No such invitation exists"
msgstr ""
#: snikket_web/admin.py:290
#: snikket_web/admin.py:289
msgid "Invitation revoked"
msgstr ""
#: snikket_web/admin.py:307 snikket_web/admin.py:355
#: snikket_web/admin.py:306 snikket_web/admin.py:354
msgid "Name"
msgstr ""
#: snikket_web/admin.py:312 snikket_web/templates/admin_circles.html:47
#: snikket_web/admin.py:311 snikket_web/templates/admin_circles.html:47
msgid "Create circle"
msgstr ""
#: snikket_web/admin.py:342
#: snikket_web/admin.py:341
msgid "Circle created"
msgstr ""
#: snikket_web/admin.py:360
#: snikket_web/admin.py:359
msgid "Select user"
msgstr ""
#: snikket_web/admin.py:365
#: snikket_web/admin.py:364
msgid "Update circle"
msgstr ""
#: snikket_web/admin.py:369
#: snikket_web/admin.py:368
msgid "Delete circle permanently"
msgstr ""
#: snikket_web/admin.py:375
#: snikket_web/admin.py:374
msgid "Add user"
msgstr ""
#: snikket_web/admin.py:391
#: snikket_web/admin.py:390
msgid "No such circle exists"
msgstr ""
#: snikket_web/admin.py:428
#: snikket_web/admin.py:427
msgid "Circle data updated"
msgstr ""
#: snikket_web/admin.py:434
#: snikket_web/admin.py:433
msgid "Circle deleted"
msgstr ""
#: snikket_web/admin.py:445
#: snikket_web/admin.py:444
msgid "User added to circle"
msgstr ""
#: snikket_web/admin.py:454
#: snikket_web/admin.py:453
msgid "User removed from circle"
msgstr ""
#: snikket_web/infra.py:40
#: snikket_web/infra.py:41
msgid "Main"
msgstr ""
@@ -153,7 +153,7 @@ msgstr ""
msgid "Username"
msgstr ""
#: snikket_web/invite.py:110 snikket_web/invite.py:177 snikket_web/main.py:42
#: snikket_web/invite.py:110 snikket_web/invite.py:177 snikket_web/main.py:41
msgid "Password"
msgstr ""
@@ -170,15 +170,15 @@ msgid "Create account"
msgstr ""
#: snikket_web/invite.py:150
msgid "That username is already taken"
msgid "That username is already taken."
msgstr ""
#: snikket_web/invite.py:154 snikket_web/invite.py:218
msgid "Registration was declined for unknown reasons"
msgid "Registration was declined for unknown reasons."
msgstr ""
#: snikket_web/invite.py:158
msgid "The username is not valid"
msgid "The username is not valid."
msgstr ""
#: snikket_web/invite.py:190 snikket_web/templates/user_home.html:32
@@ -186,90 +186,90 @@ msgstr ""
msgid "Change password"
msgstr ""
#: snikket_web/main.py:37
#: snikket_web/main.py:36
msgid "Address"
msgstr ""
#: snikket_web/main.py:47
#: snikket_web/main.py:46
msgid "Sign in"
msgstr ""
#: snikket_web/main.py:56
#: snikket_web/main.py:55
msgid "Invalid username or password."
msgstr ""
#: snikket_web/main.py:84
#: snikket_web/main.py:83
msgid "Login successful!"
msgstr ""
#: snikket_web/user.py:29
#: snikket_web/user.py:27
msgid "Current password"
msgstr ""
#: snikket_web/user.py:34
#: snikket_web/user.py:32
msgid "New password"
msgstr ""
#: snikket_web/user.py:39
#: snikket_web/user.py:37
msgid "Confirm new password"
msgstr ""
#: snikket_web/user.py:43
#: snikket_web/user.py:41
msgid "The new passwords must match"
msgstr ""
#: snikket_web/user.py:50
#: snikket_web/user.py:48
msgid "Sign out"
msgstr ""
#: snikket_web/user.py:55
#: snikket_web/user.py:53
msgid "Nobody"
msgstr ""
#: snikket_web/user.py:56
#: snikket_web/user.py:54
msgid "Friends only"
msgstr ""
#: snikket_web/user.py:57
#: snikket_web/user.py:55
msgid "Everyone"
msgstr ""
#: snikket_web/templates/admin_delete_user.html:12
#: snikket_web/templates/admin_users.html:11 snikket_web/user.py:63
#: snikket_web/templates/admin_users.html:11 snikket_web/user.py:61
msgid "Display name"
msgstr ""
#: snikket_web/user.py:67
#: snikket_web/user.py:65
msgid "Avatar"
msgstr ""
#: snikket_web/user.py:71
#: snikket_web/user.py:69
msgid "Profile visibility"
msgstr ""
#: snikket_web/user.py:76
#: snikket_web/user.py:74
msgid "Update profile"
msgstr ""
#: snikket_web/user.py:101
msgid "Incorrect password"
#: snikket_web/user.py:99
msgid "Incorrect password."
msgstr ""
#: snikket_web/user.py:105
#: snikket_web/user.py:103
msgid "Password changed"
msgstr ""
#: snikket_web/user.py:113
#: snikket_web/user.py:111
msgid ""
"The chosen avatar is too big. To be able to upload larger avatars, please"
" use the app"
" use the app."
msgstr ""
#: snikket_web/user.py:161
#: snikket_web/user.py:159
msgid "Profile updated"
msgstr ""
#: snikket_web/templates/unauth.html:18 snikket_web/user.py:169
#: snikket_web/templates/unauth.html:18 snikket_web/user.py:167
msgid "Error"
msgstr ""
@@ -789,19 +789,20 @@ msgid ""
msgstr ""
#: snikket_web/templates/invite_register.html:14
#: snikket_web/templates/invite_view.html:38
#: snikket_web/templates/invite_view.html:39
msgid "App already installed?"
msgstr ""
#: snikket_web/templates/invite_register.html:16
#: snikket_web/templates/invite_reset_view.html:21
#: snikket_web/templates/invite_view.html:40
#: snikket_web/templates/invite_view.html:105
#: snikket_web/templates/invite_view.html:41
#: snikket_web/templates/invite_view.html:106
#: snikket_web/templates/invite_view.html:134
msgid "Open the app"
msgstr ""
#: snikket_web/templates/invite_register.html:18
#: snikket_web/templates/invite_view.html:42
#: snikket_web/templates/invite_view.html:43
msgid "This button works only if you have the app installed already!"
msgstr ""
@@ -887,7 +888,7 @@ msgid ""
msgstr ""
#: snikket_web/templates/invite_reset_view.html:26
#: snikket_web/templates/invite_view.html:76
#: snikket_web/templates/invite_view.html:77
msgid ""
"Your camera will turn on. Point it at the square code below until it is "
"within the highlighted square on your screen, and wait until the app "
@@ -899,7 +900,7 @@ msgid "You will then be prompted to enter a new password for your account."
msgstr ""
#: snikket_web/templates/invite_reset_view.html:29
#: snikket_web/templates/invite_view.html:44
#: snikket_web/templates/invite_view.html:45
msgid "Alternatives"
msgstr ""
@@ -975,21 +976,25 @@ msgid "Get it on Google Play"
msgstr ""
#: snikket_web/templates/invite_view.html:30
#: snikket_web/templates/invite_view.html:101
#: snikket_web/templates/invite_view.html:102
msgid "Download on the App Store"
msgstr ""
#: snikket_web/templates/invite_view.html:34
#: snikket_web/templates/invite_view.html:32
msgid "Get it on F-Droid"
msgstr ""
#: snikket_web/templates/invite_view.html:35
msgid "Send to mobile device"
msgstr ""
#: snikket_web/templates/invite_view.html:37
#: snikket_web/templates/invite_view.html:38
msgid ""
"After installation the app should automatically open and prompt you to "
"create an account. If not, simply click the button below."
msgstr ""
#: snikket_web/templates/invite_view.html:45
#: snikket_web/templates/invite_view.html:46
#, python-format
msgid ""
"You can connect to Snikket using any XMPP-compatible software. If the "
@@ -997,64 +1002,82 @@ msgid ""
"href=\"%(register_url)s\">register an account manually</a>."
msgstr ""
#: snikket_web/templates/invite_view.html:51
#: snikket_web/templates/invite_view.html:52
msgid "Scan invite code"
msgstr ""
#: snikket_web/templates/invite_view.html:54
#: snikket_web/templates/invite_view.html:83
#: snikket_web/templates/invite_view.html:95
#: snikket_web/templates/invite_view.html:111
#: snikket_web/templates/invite_view.html:55
#: snikket_web/templates/invite_view.html:84
#: snikket_web/templates/invite_view.html:96
#: snikket_web/templates/invite_view.html:112
#: snikket_web/templates/invite_view.html:124
#: snikket_web/templates/invite_view.html:140
msgid "Close"
msgstr ""
#: snikket_web/templates/invite_view.html:57
#: snikket_web/templates/invite_view.html:58
msgid ""
"You can transfer this invite to your mobile device by scanning a code "
"with your camera. You can use either a QR scanner app or the Snikket app "
"itself."
msgstr ""
#: snikket_web/templates/invite_view.html:62
#: snikket_web/templates/invite_view.html:63
msgid "Using a QR code scanner"
msgstr ""
#: snikket_web/templates/invite_view.html:64
#: snikket_web/templates/invite_view.html:65
msgid "Using the Snikket app"
msgstr ""
#: snikket_web/templates/invite_view.html:69
#: snikket_web/templates/invite_view.html:70
msgid ""
"Use a <em>QR code</em> scanner on your mobile device to scan the code "
"below:"
msgstr ""
#: snikket_web/templates/invite_view.html:75
#: snikket_web/templates/invite_view.html:76
msgid ""
"Install the Snikket app on your mobile device, open it, and tap the "
"'Scan' button at the top."
msgstr ""
#: snikket_web/templates/invite_view.html:92
#: snikket_web/templates/invite_view.html:93
msgid "Install on iOS"
msgstr ""
#: snikket_web/templates/invite_view.html:98
#: snikket_web/templates/invite_view.html:99
msgid ""
"After downloading Snikket from the app store, you have to return to this "
"invite link and tap on \"Open the app\" to proceed."
msgstr ""
#: snikket_web/templates/invite_view.html:100
#: snikket_web/templates/invite_view.html:101
msgid "First download Snikket from the app store using the button below:"
msgstr ""
#: snikket_web/templates/invite_view.html:102
#: snikket_web/templates/invite_view.html:103
#: snikket_web/templates/invite_view.html:131
msgid ""
"After the installation is complete, you can return to this page and tap "
"the \"Open the app\" button to continue with the setup:"
msgstr ""
#: snikket_web/templates/invite_view.html:121
#: snikket_web/templates/invite_view.html:130
msgid "Install via F-Droid"
msgstr ""
#: snikket_web/templates/invite_view.html:127
msgid ""
"After installing Snikket via F-Droid, you have to return to this invite "
"link and tap on \"Open the app\" to proceed."
msgstr ""
#: snikket_web/templates/invite_view.html:129
msgid "First install Snikket from F-Droid using the button below:"
msgstr ""
#: snikket_web/templates/library.j2:18
msgid "Copy link"
msgstr ""
@@ -1083,15 +1106,11 @@ msgstr ""
msgid "Enter your Snikket address and password to manage your account."
msgstr ""
#: snikket_web/templates/login.html:18
msgid "Login failed"
msgstr ""
#: snikket_web/templates/login.html:23
#: snikket_web/templates/login.html:19
msgid "Incorrect address"
msgstr ""
#: snikket_web/templates/login.html:24
#: snikket_web/templates/login.html:20
#, python-format
msgid ""
"This Snikket service only hosts addresses ending in "

View File

@@ -15,16 +15,14 @@ import quart.exceptions
import wtforms
import flask_wtf
from flask_babel import lazy_gettext as _l, _
from .infra import client
from .infra import client, BaseForm
bp = Blueprint('user', __name__)
class ChangePasswordForm(flask_wtf.FlaskForm): # type:ignore
class ChangePasswordForm(BaseForm):
current_password = wtforms.PasswordField(
_l("Current password"),
validators=[wtforms.validators.InputRequired()]
@@ -45,7 +43,7 @@ class ChangePasswordForm(flask_wtf.FlaskForm): # type:ignore
)
class LogoutForm(flask_wtf.FlaskForm): # type:ignore
class LogoutForm(BaseForm):
action_signout = wtforms.SubmitField(
_l("Sign out"),
)
@@ -58,7 +56,7 @@ _ACCESS_MODEL_CHOICES = [
]
class ProfileForm(flask_wtf.FlaskForm): # type:ignore
class ProfileForm(BaseForm):
nickname = wtforms.TextField(
_l("Display name"),
)
@@ -98,7 +96,7 @@ async def change_pw() -> typing.Union[str, quart.Response]:
quart.exceptions.Forbidden):
# server refused current password, set an appropriate error
form.current_password.errors.append(
_("Incorrect password"),
_("Incorrect password."),
)
else:
await flash(
@@ -112,7 +110,7 @@ async def change_pw() -> typing.Union[str, quart.Response]:
EAVATARTOOBIG = _l(
"The chosen avatar is too big. To be able to upload larger "
"avatars, please use the app"
"avatars, please use the app."
)