You've already forked snikket-web-portal
Fix error handling
Previously, some kinds of errors would throw nice and fun cascades of exceptions. We now have a nice, clean error page for 500 and 503 (backend connectivity) errors which includes minimal debugging information for productive setups and a traceback for development setups. In any case, the full exception is logged to the log with an error ID which is printed on the error page.
This commit is contained in:
@@ -5,11 +5,16 @@ import os
|
||||
import pathlib
|
||||
import typing
|
||||
|
||||
import aiohttp
|
||||
|
||||
import quart.flask_patch
|
||||
|
||||
import quart
|
||||
from quart import (
|
||||
url_for,
|
||||
render_template,
|
||||
current_app,
|
||||
redirect,
|
||||
)
|
||||
|
||||
import environ
|
||||
@@ -18,7 +23,7 @@ from . import colour, infra
|
||||
from ._version import version, version_info # noqa:F401
|
||||
|
||||
|
||||
def proc() -> typing.Dict[str, typing.Any]:
|
||||
async def proc() -> typing.Dict[str, typing.Any]:
|
||||
def url_for_avatar(entity: str, hash_: str,
|
||||
**kwargs: typing.Any) -> str:
|
||||
return url_for(
|
||||
@@ -32,10 +37,16 @@ def proc() -> typing.Dict[str, typing.Any]:
|
||||
**kwargs
|
||||
)
|
||||
|
||||
try:
|
||||
user_info = await infra.client.get_user_info()
|
||||
except (aiohttp.ClientError, quart.exceptions.HTTPException):
|
||||
user_info = {}
|
||||
|
||||
return {
|
||||
"url_for_avatar": url_for_avatar,
|
||||
"text_to_css": colour.text_to_css,
|
||||
"lang": infra.selected_locale(),
|
||||
"user_info": user_info,
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +56,85 @@ def autosplit(s: typing.Union[str, typing.List[str]]) -> typing.List[str]:
|
||||
return s
|
||||
|
||||
|
||||
async def render_exception_template(
|
||||
template: str,
|
||||
exc: Exception,
|
||||
error_id: str,
|
||||
) -> str:
|
||||
more: typing.Dict[str, str] = {}
|
||||
if current_app.debug:
|
||||
import traceback
|
||||
more.update(
|
||||
traceback="".join(traceback.format_exception(
|
||||
type(exc),
|
||||
exc,
|
||||
exc.__traceback__,
|
||||
)),
|
||||
)
|
||||
|
||||
return await render_template(
|
||||
template,
|
||||
exception_short=str(
|
||||
".".join([
|
||||
type(exc).__module__,
|
||||
type(exc).__qualname__,
|
||||
]),
|
||||
),
|
||||
error_id=error_id,
|
||||
**more,
|
||||
)
|
||||
|
||||
|
||||
async def backend_error_handler(exc: Exception) -> quart.Response:
|
||||
error_id = infra.generate_error_id()
|
||||
current_app.logger.error(
|
||||
"error_id=%s returning 503 status page for exception",
|
||||
error_id,
|
||||
exc_info=exc,
|
||||
)
|
||||
return quart.Response(
|
||||
await render_exception_template(
|
||||
"backend_error.html",
|
||||
exc,
|
||||
error_id,
|
||||
),
|
||||
status=503,
|
||||
)
|
||||
|
||||
|
||||
async def generic_http_error(
|
||||
exc: quart.exceptions.HTTPException,
|
||||
) -> quart.Response:
|
||||
return quart.Response(
|
||||
await render_template(
|
||||
"generic_http_error.html",
|
||||
status=exc.status_code,
|
||||
description=exc.description,
|
||||
name=exc.name,
|
||||
),
|
||||
status=exc.status_code,
|
||||
)
|
||||
|
||||
|
||||
async def generic_error_handler(
|
||||
exc: Exception,
|
||||
) -> quart.Response:
|
||||
error_id = infra.generate_error_id()
|
||||
current_app.logger.error(
|
||||
"error_id=%s returning 500 status page for exception",
|
||||
error_id,
|
||||
exc_info=exc,
|
||||
)
|
||||
return quart.Response(
|
||||
await render_exception_template(
|
||||
"internal_error.html",
|
||||
exc,
|
||||
error_id,
|
||||
),
|
||||
status=500,
|
||||
)
|
||||
|
||||
|
||||
@environ.config(prefix="SNIKKET_WEB")
|
||||
class AppConfig:
|
||||
secret_key = environ.var()
|
||||
@@ -82,6 +172,25 @@ def create_app() -> quart.Quart:
|
||||
app.config["AVATAR_CACHE_TTL"] = config.avatar_cache_ttl
|
||||
|
||||
app.context_processor(proc)
|
||||
app.register_error_handler(
|
||||
aiohttp.ClientConnectorError,
|
||||
backend_error_handler, # type:ignore
|
||||
)
|
||||
app.register_error_handler(
|
||||
quart.exceptions.HTTPException,
|
||||
generic_http_error, # type:ignore
|
||||
)
|
||||
app.register_error_handler(
|
||||
Exception,
|
||||
generic_error_handler, # type:ignore
|
||||
)
|
||||
|
||||
@app.route("/")
|
||||
async def index() -> quart.Response:
|
||||
if infra.client.has_session:
|
||||
return redirect(url_for('user.index'))
|
||||
|
||||
return redirect(url_for('main.login'))
|
||||
|
||||
logging_config = app.config.get("LOGGING_CONFIG")
|
||||
if logging_config is not None:
|
||||
|
||||
@@ -30,14 +30,12 @@ bp = Blueprint("admin", __name__, url_prefix="/admin")
|
||||
@bp.route("/")
|
||||
@client.require_admin_session()
|
||||
async def index() -> str:
|
||||
user_info = await client.get_user_info()
|
||||
return await render_template("admin_home.html", user_info=user_info)
|
||||
return await render_template("admin_home.html")
|
||||
|
||||
|
||||
@bp.route("/users")
|
||||
@client.require_admin_session()
|
||||
async def users() -> str:
|
||||
user_info = await client.get_user_info()
|
||||
users = sorted(
|
||||
await client.list_users(),
|
||||
key=lambda x: x.localpart
|
||||
@@ -45,7 +43,6 @@ async def users() -> str:
|
||||
return await render_template(
|
||||
"admin_users.html",
|
||||
users=users,
|
||||
user_info=user_info,
|
||||
)
|
||||
|
||||
|
||||
@@ -58,7 +55,6 @@ class DeleteUserForm(flask_wtf.FlaskForm): # type:ignore
|
||||
@bp.route("/user/<localpart>/delete", methods=["GET", "POST"])
|
||||
@client.require_admin_session()
|
||||
async def delete_user(localpart: str) -> typing.Union[str, quart.Response]:
|
||||
user_info = await client.get_user_info()
|
||||
target_user_info = await client.get_user_by_localpart(localpart)
|
||||
form = DeleteUserForm()
|
||||
if form.validate_on_submit():
|
||||
@@ -69,7 +65,6 @@ async def delete_user(localpart: str) -> typing.Union[str, quart.Response]:
|
||||
return await render_template(
|
||||
"admin_delete_user.html",
|
||||
target_user=target_user_info,
|
||||
user_info=user_info,
|
||||
form=form,
|
||||
)
|
||||
|
||||
@@ -130,7 +125,6 @@ class InvitePost(flask_wtf.FlaskForm): # type:ignore
|
||||
@bp.route("/invitations", methods=["GET", "POST"])
|
||||
@client.require_admin_session()
|
||||
async def invitations() -> typing.Union[str, quart.Response]:
|
||||
user_info = await client.get_user_info()
|
||||
invites = sorted(
|
||||
await client.list_invites(),
|
||||
key=lambda x: x.created_at,
|
||||
@@ -156,7 +150,6 @@ async def invitations() -> typing.Union[str, quart.Response]:
|
||||
|
||||
return await render_template(
|
||||
"admin_invites.html",
|
||||
user_info=user_info,
|
||||
invites=invites,
|
||||
invite_form=invite_form,
|
||||
now=datetime.utcnow(),
|
||||
@@ -174,7 +167,6 @@ class InviteForm(flask_wtf.FlaskForm): # type:ignore
|
||||
@bp.route("/invitation/-/new", methods=["POST"])
|
||||
@client.require_admin_session()
|
||||
async def create_invite() -> typing.Union[str, quart.Response]:
|
||||
user_info = await client.get_user_info()
|
||||
form = InvitePost()
|
||||
circles = await client.list_groups()
|
||||
form.circles.choices = [
|
||||
@@ -188,14 +180,12 @@ async def create_invite() -> typing.Union[str, quart.Response]:
|
||||
)
|
||||
return redirect(url_for(".edit_invite", id_=invite.id_))
|
||||
return await render_template("admin_create_invite.html",
|
||||
user_info=user_info,
|
||||
invite_form=form)
|
||||
|
||||
|
||||
@bp.route("/invitation/<id_>", methods=["GET", "POST"])
|
||||
@client.require_admin_session()
|
||||
async def edit_invite(id_: str) -> typing.Union[str, quart.Response]:
|
||||
user_info = await client.get_user_info()
|
||||
invite_info = await client.get_invite_by_id(id_)
|
||||
circles = await client.list_groups()
|
||||
circle_map = {
|
||||
@@ -212,7 +202,6 @@ async def edit_invite(id_: str) -> typing.Union[str, quart.Response]:
|
||||
|
||||
return await render_template(
|
||||
"admin_edit_invite.html",
|
||||
user_info=user_info,
|
||||
invite=invite_info,
|
||||
now=datetime.utcnow(),
|
||||
form=form,
|
||||
@@ -233,7 +222,6 @@ class CirclePost(flask_wtf.FlaskForm): # type:ignore
|
||||
@bp.route("/circles")
|
||||
@client.require_admin_session()
|
||||
async def circles() -> str:
|
||||
user_info = await client.get_user_info()
|
||||
circles = sorted(
|
||||
await client.list_groups(),
|
||||
key=lambda x: x.name
|
||||
@@ -243,7 +231,6 @@ async def circles() -> str:
|
||||
return await render_template(
|
||||
"admin_circles.html",
|
||||
circles=circles,
|
||||
user_info=user_info,
|
||||
invite_form=invite_form,
|
||||
create_form=create_form,
|
||||
)
|
||||
@@ -252,7 +239,6 @@ async def circles() -> str:
|
||||
@bp.route("/circle/-/new", methods=["POST"])
|
||||
@client.require_admin_session()
|
||||
async def create_circle() -> typing.Union[str, quart.Response]:
|
||||
user_info = await client.get_user_info()
|
||||
create_form = CirclePost()
|
||||
if create_form.validate_on_submit():
|
||||
circle = await client.create_group(
|
||||
@@ -262,7 +248,6 @@ async def create_circle() -> typing.Union[str, quart.Response]:
|
||||
|
||||
return await render_template(
|
||||
"admin_create_circle.html",
|
||||
user_info=user_info,
|
||||
create_form=create_form,
|
||||
)
|
||||
|
||||
@@ -287,9 +272,6 @@ class EditCircleForm(flask_wtf.FlaskForm): # type:ignore
|
||||
@client.require_admin_session()
|
||||
async def edit_circle(id_: str) -> typing.Union[str, quart.Response]:
|
||||
async with client.authenticated_session() as session:
|
||||
user_info = await client.get_user_info(
|
||||
session=session,
|
||||
)
|
||||
try:
|
||||
circle = await client.get_group_by_id(
|
||||
id_,
|
||||
@@ -328,7 +310,6 @@ async def edit_circle(id_: str) -> typing.Union[str, quart.Response]:
|
||||
return await render_template(
|
||||
"admin_edit_circle.html",
|
||||
target_circle=circle,
|
||||
user_info=user_info,
|
||||
form=form,
|
||||
circle_members=circle_members,
|
||||
invite_form=invite_form,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import base64
|
||||
import itertools
|
||||
import secrets
|
||||
import typing
|
||||
|
||||
import quart.flask_patch # noqa:F401
|
||||
@@ -48,3 +50,9 @@ def init_templating(app: quart.Quart) -> None:
|
||||
app.template_filter("format_timedelta")(flask_babel.format_timedelta)
|
||||
app.template_filter("flatten")(flatten)
|
||||
app.template_filter("circle_name")(circle_name)
|
||||
|
||||
|
||||
def generate_error_id() -> str:
|
||||
return base64.b32encode(secrets.token_bytes(8)).decode(
|
||||
"ascii"
|
||||
).rstrip("=")
|
||||
|
||||
@@ -914,3 +914,11 @@ ul.inline {
|
||||
body.no-copy .copy-to-clipboard {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
/* magic */
|
||||
|
||||
pre.guru-meditation {
|
||||
width: 100%;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
{% extends "unauth.html" %}
|
||||
{% from "library.j2" import avatar with context %}
|
||||
{% block head_lead %}
|
||||
<title>{% trans %}Snikket Web Portal{% endtrans %}</title>
|
||||
{% endblock %}
|
||||
{% block style %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/app.css') }}">
|
||||
{{ super() }}
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<div id="topbar" class="{% block topbar_classes %}{% endblock %}">
|
||||
<header><a href="{{ url_for('.index') }}"><span>{{ config["SNIKKET_DOMAIN"] }}</span></a></header>
|
||||
{% block topbar_left %}{% endblock %}
|
||||
<div class="filler"></div>
|
||||
{% block topbar_right %}{% endblock %}
|
||||
<nav class="usermenu">{{ user_info.display_name }}{% call avatar(user_info.address, user_info.avatar_hash ) %}{% endcall %}</nav>
|
||||
</div>
|
||||
<div id="mwrap"><main>{% block content %}{% endblock %}</main></div>
|
||||
<footer>
|
||||
<ul><li>{% trans about_url=url_for('main.about') %}A <a href="{{ about_url }}">Snikket</a> server{% endtrans %}</li></ul>
|
||||
</footer>
|
||||
{% endblock %}
|
||||
{% block topbar_right %}
|
||||
{{- super() -}}
|
||||
<nav class="usermenu">{{ user_info.display_name }}{% call avatar(user_info.address, user_info.avatar_hash ) %}{% endcall %}</nav>
|
||||
{%- endblock %}
|
||||
|
||||
6
snikket_web/templates/backend_error.html
Normal file
6
snikket_web/templates/backend_error.html
Normal file
@@ -0,0 +1,6 @@
|
||||
{% extends "exception.html" %}
|
||||
{% block box -%}
|
||||
<header>{% trans %}Internal error{% endtrans %}</header>
|
||||
<p>{% trans %}The web portal was not able to communicate with the backend.{% endtrans %}</p>
|
||||
<p>{% trans %}Please try again later and/or inform your Snikket instance admin.{% endtrans %}</p>
|
||||
{%- endblock %}
|
||||
17
snikket_web/templates/exception.html
Normal file
17
snikket_web/templates/exception.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends "unauth.html" %}
|
||||
{% block head_lead -%}
|
||||
<title>{% trans %}Internal error{% endtrans %}</title>
|
||||
{%- endblock %}
|
||||
{% block content %}
|
||||
<div class="box alert el-3">
|
||||
{%- block box %}{% endblock -%}
|
||||
<pre class="guru-meditation">
|
||||
GURU MEDITATION
|
||||
{% if traceback %}
|
||||
{{- traceback -}}
|
||||
{% else %}
|
||||
{{- exception_short -}}
|
||||
{% endif %}
|
||||
error_id={{ error_id }}</pre>
|
||||
</div>
|
||||
{% endblock %}
|
||||
13
snikket_web/templates/generic_http_error.html
Normal file
13
snikket_web/templates/generic_http_error.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends "unauth.html" %}
|
||||
{% from "library.j2" import standard_button %}
|
||||
{% block content -%}
|
||||
<div class="box alert el-3">
|
||||
<header>{{ name }}</header>
|
||||
<p>{{ description }}.</p>
|
||||
<div class="f-bbox">
|
||||
{%- call standard_button("back", url_for("index"), class="primary") -%}
|
||||
{% trans %}Go back to the main page{% endtrans %}
|
||||
{%- endcall -%}
|
||||
</div>
|
||||
</div>
|
||||
{%- endblock %}
|
||||
6
snikket_web/templates/internal_error.html
Normal file
6
snikket_web/templates/internal_error.html
Normal file
@@ -0,0 +1,6 @@
|
||||
{% extends "exception.html" %}
|
||||
{% block box -%}
|
||||
<header>{% trans %}Internal error{% endtrans %}</header>
|
||||
<p>{% trans %}The web portal encountered an internal error.{% endtrans %}</p>
|
||||
<p>{% trans %}Please try again later and/or inform your Snikket instance admin.{% endtrans %}</p>
|
||||
{%- endblock %}
|
||||
18
snikket_web/templates/unauth.html
Normal file
18
snikket_web/templates/unauth.html
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "library.j2" import box, form_button %}
|
||||
{% block style %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/app.css') }}">
|
||||
{{ super() }}
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<div id="topbar" class="{% block topbar_classes %}{% endblock %}">
|
||||
<header><a href="{{ url_for('.index') }}"><span>{{ config["SNIKKET_DOMAIN"] }}</span></a></header>
|
||||
{% block topbar_left %}{% endblock %}
|
||||
<div class="filler"></div>
|
||||
{% block topbar_right %}{% endblock %}
|
||||
</div>
|
||||
<div id="mwrap"><main>{% block content %}{% endblock %}</main></div>
|
||||
<footer>
|
||||
<ul><li>{% trans about_url=url_for('main.about') %}A <a href="{{ about_url }}">Snikket</a> server{% endtrans %}</li></ul>
|
||||
</footer>
|
||||
{% endblock %}
|
||||
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SnikketWeb 0.1.0\n"
|
||||
"Report-Msgid-Bugs-To: jonas@zombofant.net\n"
|
||||
"POT-Creation-Date: 2021-01-21 16:54+0100\n"
|
||||
"POT-Creation-Date: 2021-01-21 16:55+0100\n"
|
||||
"PO-Revision-Date: 2020-03-07 16:32+0100\n"
|
||||
"Last-Translator: Jonas Schäfer <jonas@zombofant.net>\n"
|
||||
"Language: de\n"
|
||||
@@ -18,71 +18,71 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.9.0\n"
|
||||
|
||||
#: snikket_web/admin.py:54
|
||||
#: snikket_web/admin.py:51
|
||||
msgid "Delete user permanently"
|
||||
msgstr "Benutzer endgültig löschen"
|
||||
|
||||
#: snikket_web/admin.py:83
|
||||
#: snikket_web/admin.py:78
|
||||
msgid "Invite to circle"
|
||||
msgstr "In Gemeinschaft einladen"
|
||||
|
||||
#: snikket_web/admin.py:89
|
||||
#: snikket_web/admin.py:84
|
||||
msgid "At least one circle must be selected"
|
||||
msgstr "Mindestens eine Gemeinschaft muss ausgewählt sein"
|
||||
|
||||
#: snikket_web/admin.py:94
|
||||
#: snikket_web/admin.py:89
|
||||
msgid "Valid for"
|
||||
msgstr "Gültig für"
|
||||
|
||||
#: snikket_web/admin.py:96
|
||||
#: snikket_web/admin.py:91
|
||||
msgid "One hour"
|
||||
msgstr "Eine Stunde"
|
||||
|
||||
#: snikket_web/admin.py:97
|
||||
#: snikket_web/admin.py:92
|
||||
msgid "Twelve hours"
|
||||
msgstr "Zwölf Stunden"
|
||||
|
||||
#: snikket_web/admin.py:98
|
||||
#: snikket_web/admin.py:93
|
||||
msgid "One day"
|
||||
msgstr "Ein Tag"
|
||||
|
||||
#: snikket_web/admin.py:99
|
||||
#: snikket_web/admin.py:94
|
||||
msgid "One week"
|
||||
msgstr "Eine Woche"
|
||||
|
||||
#: snikket_web/admin.py:100
|
||||
#: snikket_web/admin.py:95
|
||||
msgid "Four weeks"
|
||||
msgstr "Vier Wochen"
|
||||
|
||||
#: snikket_web/admin.py:106
|
||||
#: snikket_web/admin.py:101
|
||||
msgid "Allow multiple uses"
|
||||
msgstr "Mehrfach verwendbar"
|
||||
|
||||
#: snikket_web/admin.py:110
|
||||
#: snikket_web/admin.py:105
|
||||
msgid "New invitation link"
|
||||
msgstr "Neuer Einladungslink"
|
||||
|
||||
#: snikket_web/admin.py:170
|
||||
#: snikket_web/admin.py:163
|
||||
msgid "Revoke"
|
||||
msgstr "Löschen"
|
||||
|
||||
#: snikket_web/admin.py:225 snikket_web/admin.py:272
|
||||
#: snikket_web/admin.py:214 snikket_web/admin.py:257
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
|
||||
#: snikket_web/admin.py:229 snikket_web/templates/admin_circles.html:42
|
||||
#: snikket_web/admin.py:218 snikket_web/templates/admin_circles.html:42
|
||||
msgid "Create circle"
|
||||
msgstr "Gemeinschaft gründen"
|
||||
|
||||
#: snikket_web/admin.py:276 snikket_web/user.py:73
|
||||
#: snikket_web/admin.py:261 snikket_web/user.py:68
|
||||
msgid "Apply"
|
||||
msgstr "Übernehmen"
|
||||
|
||||
#: snikket_web/admin.py:280
|
||||
#: snikket_web/admin.py:265
|
||||
msgid "Delete circle permanently"
|
||||
msgstr "Gemeinschaft endgültig löschen"
|
||||
|
||||
#: snikket_web/infra.py:39
|
||||
#: snikket_web/infra.py:41
|
||||
msgid "Main"
|
||||
msgstr "Kern"
|
||||
|
||||
@@ -102,53 +102,53 @@ msgstr "Anmelden"
|
||||
msgid "Invalid user name or password."
|
||||
msgstr "Benutzername oder Passwort falsch."
|
||||
|
||||
#: snikket_web/user.py:26
|
||||
#: snikket_web/user.py:21
|
||||
msgid "Current password"
|
||||
msgstr "Aktuelles Passwort"
|
||||
|
||||
#: snikket_web/user.py:31
|
||||
#: snikket_web/user.py:26
|
||||
msgid "New password"
|
||||
msgstr "Neues Passwort"
|
||||
|
||||
#: snikket_web/user.py:36
|
||||
#: snikket_web/user.py:31
|
||||
msgid "Confirm new password"
|
||||
msgstr "Neues Passwort (Bestätigung)"
|
||||
|
||||
#: snikket_web/user.py:40
|
||||
#: snikket_web/user.py:35
|
||||
msgid "The new passwords must match."
|
||||
msgstr "Die neuen Passwörter müssen übereinstimmen."
|
||||
|
||||
#: snikket_web/user.py:47
|
||||
#: snikket_web/user.py:42
|
||||
msgid "Sign out"
|
||||
msgstr "Abmelden"
|
||||
|
||||
#: snikket_web/user.py:52
|
||||
#: snikket_web/user.py:47
|
||||
msgid "Nobody"
|
||||
msgstr "Niemand"
|
||||
|
||||
#: snikket_web/user.py:53
|
||||
#: snikket_web/user.py:48
|
||||
msgid "Friends only"
|
||||
msgstr "Nur Freunde"
|
||||
|
||||
#: snikket_web/user.py:54
|
||||
#: snikket_web/user.py:49
|
||||
msgid "Everyone"
|
||||
msgstr "Jeder"
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:12
|
||||
#: snikket_web/templates/admin_delete_user.html:16
|
||||
#: snikket_web/templates/admin_users.html:9 snikket_web/user.py:60
|
||||
#: snikket_web/templates/admin_users.html:9 snikket_web/user.py:55
|
||||
msgid "Display name"
|
||||
msgstr "Anzeigename"
|
||||
|
||||
#: snikket_web/user.py:64
|
||||
#: snikket_web/user.py:59
|
||||
msgid "Avatar"
|
||||
msgstr "Bild"
|
||||
|
||||
#: snikket_web/user.py:68
|
||||
#: snikket_web/user.py:63
|
||||
msgid "Profile visibility"
|
||||
msgstr "Profilsichtbarkeit"
|
||||
|
||||
#: snikket_web/user.py:97
|
||||
#: snikket_web/user.py:92
|
||||
msgid "Incorrect password"
|
||||
msgstr "Ungültiges Passwort"
|
||||
|
||||
@@ -438,10 +438,22 @@ msgstr "Telefonnummer"
|
||||
msgid "Snikket Web Portal"
|
||||
msgstr "Snikket Webportal"
|
||||
|
||||
#: snikket_web/templates/app.html:20 snikket_web/templates/login.html:36
|
||||
#, python-format
|
||||
msgid "A <a href=\"%(about_url)s\">Snikket</a> server"
|
||||
msgstr "Ein <a href=\"%(about_url)s\">Snikket</a>-Server"
|
||||
#: snikket_web/templates/backend_error.html:3
|
||||
#: snikket_web/templates/exception.html:3
|
||||
#: snikket_web/templates/internal_error.html:3
|
||||
msgid "Internal error"
|
||||
msgstr "Interner Fehler"
|
||||
|
||||
#: snikket_web/templates/backend_error.html:4
|
||||
msgid "The web portal was not able to communicate with the backend."
|
||||
msgstr "Das Webportal konnte nicht mit dem Backend kommunizieren."
|
||||
|
||||
#: snikket_web/templates/backend_error.html:5
|
||||
#: snikket_web/templates/internal_error.html:5
|
||||
msgid "Please try again later and/or inform your Snikket instance admin."
|
||||
msgstr ""
|
||||
"Versuche es später noch einmal und/oder informiere den Betreiber deiner "
|
||||
"Snikket-Instanz."
|
||||
|
||||
#: snikket_web/templates/copy-snippet.html:106
|
||||
msgid "Copied to clipboard"
|
||||
@@ -451,6 +463,14 @@ msgstr "Kopiert"
|
||||
msgid "Copy operation failed"
|
||||
msgstr "Kopieren fehlgeschlagen"
|
||||
|
||||
#: snikket_web/templates/generic_http_error.html:9
|
||||
msgid "Go back to the main page"
|
||||
msgstr "Zurück zur Hauptseite"
|
||||
|
||||
#: snikket_web/templates/internal_error.html:4
|
||||
msgid "The web portal encountered an internal error."
|
||||
msgstr "Das Webportal hatte einen internen Fehler."
|
||||
|
||||
#: snikket_web/templates/library.j2:18
|
||||
msgid "Copy link"
|
||||
msgstr "Link kopieren"
|
||||
@@ -471,6 +491,11 @@ msgstr "Gib deine Snikket-Adresse und -Passwort ein um dein Konto zu verwalten."
|
||||
msgid "Login failed"
|
||||
msgstr "Anmeldung fehlgeschlagen"
|
||||
|
||||
#: snikket_web/templates/login.html:36 snikket_web/templates/unauth.html:16
|
||||
#, python-format
|
||||
msgid "A <a href=\"%(about_url)s\">Snikket</a> server"
|
||||
msgstr "Ein <a href=\"%(about_url)s\">Snikket</a>-Server"
|
||||
|
||||
#: snikket_web/templates/user_home.html:3
|
||||
msgid "Welcome!"
|
||||
msgstr "Willkommen!"
|
||||
|
||||
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2021-01-21 16:54+0100\n"
|
||||
"POT-Creation-Date: 2021-01-21 16:55+0100\n"
|
||||
"PO-Revision-Date: 2020-03-07 16:50+0100\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: en\n"
|
||||
@@ -18,71 +18,71 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.9.0\n"
|
||||
|
||||
#: snikket_web/admin.py:54
|
||||
#: snikket_web/admin.py:51
|
||||
msgid "Delete user permanently"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:83
|
||||
#: snikket_web/admin.py:78
|
||||
msgid "Invite to circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:89
|
||||
#: snikket_web/admin.py:84
|
||||
msgid "At least one circle must be selected"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:94
|
||||
#: snikket_web/admin.py:89
|
||||
msgid "Valid for"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:96
|
||||
#: snikket_web/admin.py:91
|
||||
msgid "One hour"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:97
|
||||
#: snikket_web/admin.py:92
|
||||
msgid "Twelve hours"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:98
|
||||
#: snikket_web/admin.py:93
|
||||
msgid "One day"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:99
|
||||
#: snikket_web/admin.py:94
|
||||
msgid "One week"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:100
|
||||
#: snikket_web/admin.py:95
|
||||
msgid "Four weeks"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:106
|
||||
#: snikket_web/admin.py:101
|
||||
msgid "Allow multiple uses"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:110
|
||||
#: snikket_web/admin.py:105
|
||||
msgid "New invitation link"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:170
|
||||
#: snikket_web/admin.py:163
|
||||
msgid "Revoke"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:225 snikket_web/admin.py:272
|
||||
#: snikket_web/admin.py:214 snikket_web/admin.py:257
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:229 snikket_web/templates/admin_circles.html:42
|
||||
#: snikket_web/admin.py:218 snikket_web/templates/admin_circles.html:42
|
||||
msgid "Create circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:276 snikket_web/user.py:73
|
||||
#: snikket_web/admin.py:261 snikket_web/user.py:68
|
||||
msgid "Apply"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:280
|
||||
#: snikket_web/admin.py:265
|
||||
msgid "Delete circle permanently"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/infra.py:39
|
||||
#: snikket_web/infra.py:41
|
||||
msgid "Main"
|
||||
msgstr ""
|
||||
|
||||
@@ -103,54 +103,54 @@ msgstr ""
|
||||
msgid "Invalid user name or password."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:26
|
||||
#: snikket_web/user.py:21
|
||||
msgid "Current password"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:31
|
||||
#: snikket_web/user.py:26
|
||||
msgid "New password"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:36
|
||||
#: snikket_web/user.py:31
|
||||
msgid "Confirm new password"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:40
|
||||
#: snikket_web/user.py:35
|
||||
msgid "The new passwords must match."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:47
|
||||
#: snikket_web/user.py:42
|
||||
#, fuzzy
|
||||
msgid "Sign out"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:52
|
||||
#: snikket_web/user.py:47
|
||||
msgid "Nobody"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:53
|
||||
#: snikket_web/user.py:48
|
||||
msgid "Friends only"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:54
|
||||
#: snikket_web/user.py:49
|
||||
msgid "Everyone"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:12
|
||||
#: snikket_web/templates/admin_delete_user.html:16
|
||||
#: snikket_web/templates/admin_users.html:9 snikket_web/user.py:60
|
||||
#: snikket_web/templates/admin_users.html:9 snikket_web/user.py:55
|
||||
msgid "Display name"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:64
|
||||
#: snikket_web/user.py:59
|
||||
msgid "Avatar"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:68
|
||||
#: snikket_web/user.py:63
|
||||
msgid "Profile visibility"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:97
|
||||
#: snikket_web/user.py:92
|
||||
msgid "Incorrect password"
|
||||
msgstr ""
|
||||
|
||||
@@ -425,9 +425,19 @@ msgstr ""
|
||||
msgid "Snikket Web Portal"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/app.html:20 snikket_web/templates/login.html:36
|
||||
#, python-format
|
||||
msgid "A <a href=\"%(about_url)s\">Snikket</a> server"
|
||||
#: snikket_web/templates/backend_error.html:3
|
||||
#: snikket_web/templates/exception.html:3
|
||||
#: snikket_web/templates/internal_error.html:3
|
||||
msgid "Internal error"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/backend_error.html:4
|
||||
msgid "The web portal was not able to communicate with the backend."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/backend_error.html:5
|
||||
#: snikket_web/templates/internal_error.html:5
|
||||
msgid "Please try again later and/or inform your Snikket instance admin."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/copy-snippet.html:106
|
||||
@@ -438,6 +448,14 @@ msgstr ""
|
||||
msgid "Copy operation failed"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/generic_http_error.html:9
|
||||
msgid "Go back to the main page"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/internal_error.html:4
|
||||
msgid "The web portal encountered an internal error."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/library.j2:18
|
||||
msgid "Copy link"
|
||||
msgstr ""
|
||||
@@ -458,6 +476,11 @@ msgstr ""
|
||||
msgid "Login failed"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/login.html:36 snikket_web/templates/unauth.html:16
|
||||
#, python-format
|
||||
msgid "A <a href=\"%(about_url)s\">Snikket</a> server"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/user_home.html:3
|
||||
msgid "Welcome!"
|
||||
msgstr ""
|
||||
|
||||
@@ -16,11 +16,6 @@ from .infra import client
|
||||
bp = Blueprint('user', __name__)
|
||||
|
||||
|
||||
@bp.context_processor
|
||||
async def proc() -> typing.Mapping[str, typing.Any]:
|
||||
return {"user_info": await client.get_user_info()}
|
||||
|
||||
|
||||
class ChangePasswordForm(flask_wtf.FlaskForm): # type:ignore
|
||||
current_password = wtforms.PasswordField(
|
||||
_l("Current password"),
|
||||
@@ -146,6 +141,6 @@ async def logout() -> typing.Union[quart.Response, str]:
|
||||
form = LogoutForm()
|
||||
if form.validate_on_submit():
|
||||
await client.logout()
|
||||
return redirect(url_for("main.home"))
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
return await render_template("user_logout.html", form=form)
|
||||
|
||||
Reference in New Issue
Block a user