From fa700bba07d13b18d45cc5292fa4ceec17cc38d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Sun, 17 Jan 2021 10:18:20 +0100 Subject: [PATCH] Massive code cleanup - Avoid fighting import cycles using a factory function - Collapse useless subpackages into simple modules - Move flask plugins / infrastructure in own module - Refactor how blueprints are used to localize information about URL routing to app factory --- .envrc | 2 +- docker/entrypoint.sh | 3 +- mypy.ini | 4 +- snikket_web/__init__.py | 195 +++--------------- snikket_web/{admin/__init__.py => admin.py} | 8 +- snikket_web/infra.py | 40 ++++ snikket_web/main.py | 130 ++++++++++++ snikket_web/prosodyclient.py | 15 +- snikket_web/templates/about.html | 2 +- snikket_web/templates/app.html | 2 +- snikket_web/templates/login.html | 4 +- snikket_web/templates/user_home.html | 6 +- snikket_web/templates/user_passwd.html | 2 +- .../translations/de/LC_MESSAGES/messages.po | 92 ++++----- .../translations/en/LC_MESSAGES/messages.po | 86 ++++---- snikket_web/{user/__init__.py => user.py} | 30 +-- 16 files changed, 330 insertions(+), 291 deletions(-) rename snikket_web/{admin/__init__.py => admin.py} (94%) create mode 100644 snikket_web/infra.py create mode 100644 snikket_web/main.py rename snikket_web/{user/__init__.py => user.py} (86%) diff --git a/.envrc b/.envrc index 15e7324..c517f69 100644 --- a/.envrc +++ b/.envrc @@ -1,5 +1,5 @@ layout python3 -export QUART_APP=snikket_web:app +export QUART_APP='snikket_web:create_app()' export QUART_ENV=development export SNIKKET_WEB_CONFIG="$(pwd)/.local/web_config.py" diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 0cab526..0aa25ec 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -1,3 +1,2 @@ #!/bin/sh - -exec hypercorn -b "0.0.0.0:8000" snikket_web:app +exec hypercorn -b "0.0.0.0:8000" 'snikket_web:create_app()' diff --git a/mypy.ini b/mypy.ini index 151fe8a..702ac3c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -6,13 +6,13 @@ disallow_untyped_calls = True disallow_untyped_defs = True disallow_incomplete_defs = True #check_untyped_defs = True -disallow_untyped_decorators = False +disallow_untyped_decorators = True #disallow_any_unimported = True #disallow_any_expr = True #disallow_any_decorated = True disallow_any_explicit = False #disallow_any_generics = True -disallow_subclassing_any = False +disallow_subclassing_any = True no_implicit_optional = True warn_redundant_casts = True warn_unused_ignores = True diff --git a/snikket_web/__init__.py b/snikket_web/__init__.py index aa8dbfb..ed692d7 100644 --- a/snikket_web/__init__.py +++ b/snikket_web/__init__.py @@ -1,155 +1,25 @@ import base64 import binascii -import itertools import logging import pathlib import typing -from datetime import datetime, timedelta - import quart.flask_patch +import quart from quart import ( - Quart, request, render_template, redirect, url_for, Response, - current_app, + url_for, ) -import quart.exceptions - -from flask_wtf import FlaskForm -import wtforms -import flask_babel -from flask_babel import Babel, _, lazy_gettext as _l - -from . import colour, xmpputil -from .prosodyclient import client +from . import colour, infra from ._version import version, version_info # noqa:F401 -app = Quart(__name__) -app.config.setdefault("LANGUAGES", ["de", "en"]) -app.config.from_envvar("SNIKKET_WEB_CONFIG") -client.init_app(app) -client.default_login_redirect = "login" - -babel = Babel(app) - - -class LoginForm(FlaskForm): - address = wtforms.TextField( - _l("Address"), - validators=[wtforms.validators.InputRequired()], - ) - - password = wtforms.PasswordField( - _l("Password"), - validators=[wtforms.validators.InputRequired()], - ) - - -@babel.localeselector -def selected_locale() -> str: - return request.accept_languages.best_match( - current_app.config['LANGUAGES'] - ) - - -@app.route("/login", methods=["GET", "POST"]) -async def login() -> typing.Union[str, quart.Response]: - if client.has_session and (await client.test_session()): - return redirect(url_for('user.index')) - - form = LoginForm() - if form.validate_on_submit(): - jid = form.address.data - localpart, domain, resource = xmpputil.split_jid(jid) - if not localpart: - localpart, domain = domain, current_app.config["SNIKKET_DOMAIN"] - jid = "{}@{}".format(localpart, domain) - password = form.password.data - try: - await client.login(jid, password) - except quart.exceptions.Unauthorized: - form.password.errors.append( - _("Invalid user name or password.") - ) - else: - return redirect(url_for('user.index')) - - return await render_template("login.html", form=form) - - -@app.route("/") -async def home() -> quart.Response: - if client.has_session: - return redirect(url_for('user.index')) - - return redirect(url_for('login')) - - -@app.route("/meta/about.html") -async def about() -> str: - return await render_template("about.html", version=version) - - -@app.route("/meta/demo.html") -async def demo() -> str: - return await render_template("demo.html") - - -def repad(s: str) -> str: - return s + "=" * (4 - len(s) % 4) - - -@app.route("/avatar//") -async def avatar(from_: str, code: str) -> quart.Response: - try: - etag = request.headers["if-none-match"] - except KeyError: - etag = None - - address = base64.urlsafe_b64decode(repad(from_)).decode("utf-8") - info = await client.get_avatar(address, metadata_only=True) - bin_hash = binascii.a2b_hex(info["sha1"]) - new_etag = base64.urlsafe_b64encode(bin_hash).decode("ascii").rstrip("=") - - cache_ttl = timedelta(seconds=current_app.config.get( - "AVATAR_CACHE_TTL", - 300, - )) - - response = Response("", mimetype=info["type"]) - response.headers["etag"] = new_etag - # XXX: It seems to me that quart expects localtime(?!) in this field... - response.expires = datetime.now() + cache_ttl - response.headers["Content-Security-Policy"] = \ - "frame-ancestors 'none'; default-src 'none'; style-src 'unsafe-inline'" - - if etag is not None and new_etag == etag: - response.status_code = 304 - return response - - data = await client.get_avatar_data(address, info["sha1"]) - if data is None: - response.status_code = 404 - return response - - response.status_code = 200 - - if request.method == "HEAD": - response.content_length = len(data) - return response - - response.set_data(data) - return response - - -@app.context_processor def proc() -> typing.Dict[str, typing.Any]: def url_for_avatar(entity: str, hash_: str, **kwargs: typing.Any) -> str: return url_for( - "avatar", + "main.avatar", from_=base64.urlsafe_b64encode( entity.encode("utf-8"), ).decode("ascii").rstrip("="), @@ -162,40 +32,41 @@ def proc() -> typing.Dict[str, typing.Any]: return { "url_for_avatar": url_for_avatar, "text_to_css": colour.text_to_css, - "lang": selected_locale(), + "lang": infra.selected_locale(), } -app.template_filter("repr")(repr) -app.template_filter("format_datetime")(flask_babel.format_datetime) -app.template_filter("format_date")(flask_babel.format_date) -app.template_filter("format_time")(flask_babel.format_time) -app.template_filter("format_timedelta")(flask_babel.format_timedelta) +def create_app() -> quart.Quart: + app = quart.Quart(__name__) + app.config.setdefault("LANGUAGES", ["de", "en"]) + app.config.from_envvar("SNIKKET_WEB_CONFIG") + app.context_processor(proc) + logging_config = app.config.get("LOGGING_CONFIG") + if logging_config is not None: + if isinstance(logging_config, dict): + logging.config.dictConfig(logging_config) + elif isinstance(logging_config, (bytes, str, pathlib.Path)): + import toml + with open(logging_config, "r") as f: + logging_config = toml.load(f) + logging.config.dictConfig(logging_config) -@app.template_filter("flatten") -def flatten(a: typing.Iterable, levels: int = 1) -> typing.Iterable: - for i in range(levels): - a = itertools.chain(*a) - return a + else: + logging.basicConfig(level=logging.WARNING) + if app.debug: + logging.getLogger("snikket_web").setLevel(logging.DEBUG) + infra.babel.init_app(app) + infra.client.init_app(app) + infra.init_templating(app) -from .user import user_bp # noqa:F401,E402 -from .admin import bp as admin_bp # noqa:F401,E402 -app.register_blueprint(user_bp) -app.register_blueprint(admin_bp) + from .main import bp as main_bp + from .user import bp as user_bp + from .admin import bp as admin_bp -logging_config = app.config.get("LOGGING_CONFIG") -if logging_config is not None: - if isinstance(logging_config, dict): - logging.config.dictConfig(logging_config) - elif isinstance(logging_config, (bytes, str, pathlib.Path)): - import toml - with open(logging_config, "r") as f: - logging_config = toml.load(f) - logging.config.dictConfig(logging_config) + app.register_blueprint(main_bp) + app.register_blueprint(user_bp, url_prefix="/user") + app.register_blueprint(admin_bp, url_prefix="/admin") -else: - logging.basicConfig(level=logging.WARNING) - if app.debug: - logging.getLogger("snikket_web").setLevel(logging.DEBUG) + return app diff --git a/snikket_web/admin/__init__.py b/snikket_web/admin.py similarity index 94% rename from snikket_web/admin/__init__.py rename to snikket_web/admin.py index 42ae785..c8ad4ad 100644 --- a/snikket_web/admin/__init__.py +++ b/snikket_web/admin.py @@ -12,7 +12,7 @@ import flask_wtf from flask_babel import lazy_gettext as _l -from snikket_web.prosodyclient import client +from .infra import client bp = Blueprint("admin", __name__, url_prefix="/admin") @@ -39,7 +39,7 @@ async def users() -> str: ) -class DeleteUserForm(flask_wtf.FlaskForm): +class DeleteUserForm(flask_wtf.FlaskForm): # type:ignore action_delete = wtforms.SubmitField( _l("Delete user permanently") ) @@ -64,7 +64,7 @@ async def delete_user(localpart: str) -> typing.Union[str, quart.Response]: ) -class InvitesListForm(flask_wtf.FlaskForm): +class InvitesListForm(flask_wtf.FlaskForm): # type:ignore action_revoke = wtforms.StringField() action_create_invite = wtforms.SubmitField( @@ -99,7 +99,7 @@ async def invitations() -> typing.Union[str, quart.Response]: ) -class InviteForm(flask_wtf.FlaskForm): +class InviteForm(flask_wtf.FlaskForm): # type:ignore action_revoke = wtforms.SubmitField( _l("Revoke") ) diff --git a/snikket_web/infra.py b/snikket_web/infra.py new file mode 100644 index 0000000..8a5028e --- /dev/null +++ b/snikket_web/infra.py @@ -0,0 +1,40 @@ +import itertools +import typing + +import quart.flask_patch # noqa:F401 +from quart import ( + current_app, + request, +) + +import flask_babel + +from . import prosodyclient + + +client = prosodyclient.ProsodyClient() +client.default_login_redirect = "main.login" + +babel = flask_babel.Babel() + + +@babel.localeselector # type:ignore +def selected_locale() -> str: + return request.accept_languages.best_match( + current_app.config['LANGUAGES'] + ) + + +def flatten(a: typing.Iterable, levels: int = 1) -> typing.Iterable: + for i in range(levels): + a = itertools.chain(*a) + return a + + +def init_templating(app: quart.Quart) -> None: + app.template_filter("repr")(repr) + app.template_filter("format_datetime")(flask_babel.format_datetime) + app.template_filter("format_date")(flask_babel.format_date) + app.template_filter("format_time")(flask_babel.format_time) + app.template_filter("format_timedelta")(flask_babel.format_timedelta) + app.template_filter("flatten")(flatten) diff --git a/snikket_web/main.py b/snikket_web/main.py new file mode 100644 index 0000000..a19944c --- /dev/null +++ b/snikket_web/main.py @@ -0,0 +1,130 @@ +import base64 +import binascii +import typing + +from datetime import datetime, timedelta + +import quart +import quart.flask_patch +from quart import ( + current_app, + redirect, + url_for, + render_template, + request, + Response, +) + +import wtforms + +import flask_wtf + +from flask_babel import lazy_gettext as _l, _ + +from . import xmpputil, _version +from .infra import client + + +bp = quart.Blueprint("main", __name__) + + +class LoginForm(flask_wtf.FlaskForm): # type:ignore + address = wtforms.TextField( + _l("Address"), + validators=[wtforms.validators.InputRequired()], + ) + + password = wtforms.PasswordField( + _l("Password"), + validators=[wtforms.validators.InputRequired()], + ) + + +@bp.route("/login", methods=["GET", "POST"]) +async def login() -> typing.Union[str, quart.Response]: + if client.has_session and (await client.test_session()): + return redirect(url_for('user.index')) + + form = LoginForm() + if form.validate_on_submit(): + jid = form.address.data + localpart, domain, resource = xmpputil.split_jid(jid) + if not localpart: + localpart, domain = domain, current_app.config["SNIKKET_DOMAIN"] + jid = "{}@{}".format(localpart, domain) + password = form.password.data + try: + await client.login(jid, password) + except quart.exceptions.Unauthorized: + form.password.errors.append( + _("Invalid user name or password.") + ) + else: + return redirect(url_for('user.index')) + + return await render_template("login.html", form=form) + + +@bp.route("/") +async def home() -> quart.Response: + if client.has_session: + return redirect(url_for('user.index')) + + return redirect(url_for('.login')) + + +@bp.route("/meta/about.html") +async def about() -> str: + return await render_template("about.html", version=_version.version) + + +@bp.route("/meta/demo.html") +async def demo() -> str: + return await render_template("demo.html") + + +def repad(s: str) -> str: + return s + "=" * (4 - len(s) % 4) + + +@bp.route("/avatar//") +async def avatar(from_: str, code: str) -> quart.Response: + try: + etag = request.headers["if-none-match"] + except KeyError: + etag = None + + address = base64.urlsafe_b64decode(repad(from_)).decode("utf-8") + info = await client.get_avatar(address, metadata_only=True) + bin_hash = binascii.a2b_hex(info["sha1"]) + new_etag = base64.urlsafe_b64encode(bin_hash).decode("ascii").rstrip("=") + + cache_ttl = timedelta(seconds=current_app.config.get( + "AVATAR_CACHE_TTL", + 300, + )) + + response = Response("", mimetype=info["type"]) + response.headers["etag"] = new_etag + # XXX: It seems to me that quart expects localtime(?!) in this field... + response.expires = datetime.now() + cache_ttl + response.headers["Content-Security-Policy"] = \ + "frame-ancestors 'none'; default-src 'none'; style-src 'unsafe-inline'" + + if etag is not None and new_etag == etag: + response.status_code = 304 + return response + + data = await client.get_avatar_data(address, info["sha1"]) + if data is None: + response.status_code = 404 + return response + + response.status_code = 200 + + if request.method == "HEAD": + response.content_length = len(data) + return response + + response.set_data(data) + return response diff --git a/snikket_web/prosodyclient.py b/snikket_web/prosodyclient.py index cf30dfe..635158d 100644 --- a/snikket_web/prosodyclient.py +++ b/snikket_web/prosodyclient.py @@ -320,13 +320,13 @@ class ProsodyClient: **kwargs: typing.Any, ) -> typing.Union[T, quart.Response]: if not self.has_session or not (await self.test_session()): - nonlocal redirect_to - if redirect_to is not False: - redirect_to = \ - redirect_to or self._default_login_redirect - if not redirect_to: + redirect_to_value = redirect_to + if redirect_to_value is not False: + redirect_to_value = \ + redirect_to_value or self._default_login_redirect + if not redirect_to_value: raise abort(401, "Not Authorized") - return redirect(url_for(redirect_to)) + return redirect(url_for(redirect_to_value)) return await f(*args, **kwargs) return wrapped @@ -664,6 +664,3 @@ class ProsodyClient: return False scopes = http_session[self.SESSION_CACHED_SCOPE].split() return SCOPE_ADMIN in scopes - - -client = ProsodyClient() diff --git a/snikket_web/templates/about.html b/snikket_web/templates/about.html index 33e2ec1..022ad37 100644 --- a/snikket_web/templates/about.html +++ b/snikket_web/templates/about.html @@ -19,7 +19,7 @@ Snikket Web Portal ({{ version }}) AVATAR_CACHE_TTL = {{ config.get("AVATAR_CACHE_TTL") | repr }} SECRET_KEY = <hidden> PROSODY_ENDPOINT = <hidden> #} -

Back to main page +

Back to main page diff --git a/snikket_web/templates/app.html b/snikket_web/templates/app.html index 3600c06..c07a60f 100644 --- a/snikket_web/templates/app.html +++ b/snikket_web/templates/app.html @@ -17,6 +17,6 @@

{% block content %}{% endblock %}
-
  • {% trans about_url=url_for('about') %}A Snikket server{% endtrans %}
+
  • {% trans about_url=url_for('main.about') %}A Snikket server{% endtrans %}
{% endblock %} diff --git a/snikket_web/templates/login.html b/snikket_web/templates/login.html index 7bccd23..545d257 100644 --- a/snikket_web/templates/login.html +++ b/snikket_web/templates/login.html @@ -12,7 +12,7 @@

{{ config["SNIKKET_DOMAIN"] }}

{{ _("Enter your Snikket address and password to manage your account.") }}

-
+ {{ form.csrf_token }} {% if form.errors %} {% call box("alert", _("Login failed")) %} @@ -33,6 +33,6 @@
-
  • {% trans about_url=url_for('about') %}A Snikket server{% endtrans %}
+
  • {% trans about_url=url_for('.about') %}A Snikket server{% endtrans %}
{% endblock %} diff --git a/snikket_web/templates/user_home.html b/snikket_web/templates/user_home.html index 848870a..2ddc755 100644 --- a/snikket_web/templates/user_home.html +++ b/snikket_web/templates/user_home.html @@ -3,11 +3,11 @@

{% trans %}Welcome!{% endtrans %}

{% trans user_name=user_info.display_name %}Welcome home, {{ user_name }}.{% endtrans %}

- +

{% trans %}Update profile{% endtrans %}

{% trans %}Change display name, set avatar and configure visibility of your personal data to others.{% endtrans %}

- +

{% trans %}Change password{% endtrans %}

{% if user_info.is_admin %} @@ -16,7 +16,7 @@

{% trans %}Manage users and invitations of this Snikket instance.{% endtrans %}

{% endif %} - +

{% trans %}Log out{% endtrans %}

{% trans %}Exit the Snikket Web Portal, without logging out your other devices.{% endtrans %}

diff --git a/snikket_web/templates/user_passwd.html b/snikket_web/templates/user_passwd.html index e79cb52..5463bff 100644 --- a/snikket_web/templates/user_passwd.html +++ b/snikket_web/templates/user_passwd.html @@ -36,7 +36,7 @@

{% trans %}After changing your password, you will have to enter the new password on all of your devices.{% endtrans %}

- {% trans %}Back{% endtrans %} + {% trans %}Back{% endtrans %}
diff --git a/snikket_web/translations/de/LC_MESSAGES/messages.po b/snikket_web/translations/de/LC_MESSAGES/messages.po index c2bf4e6..df87ff7 100644 --- a/snikket_web/translations/de/LC_MESSAGES/messages.po +++ b/snikket_web/translations/de/LC_MESSAGES/messages.po @@ -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-17 20:09+0100\n" +"POT-Creation-Date: 2021-01-17 20:11+0100\n" "PO-Revision-Date: 2020-03-07 16:32+0100\n" "Last-Translator: Jonas Schäfer \n" "Language: de\n" @@ -18,30 +18,60 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.9.0\n" -#: snikket_web/__init__.py:40 -msgid "Address" -msgstr "Adresse" - -#: snikket_web/__init__.py:45 -msgid "Password" -msgstr "Passwort" - -#: snikket_web/__init__.py:74 -msgid "Invalid user name or password." -msgstr "Benutzername oder Passwort falsch." - -#: snikket_web/admin/__init__.py:44 +#: snikket_web/admin.py:44 msgid "Delete user permanently" msgstr "Benutzer endgültig löschen" -#: snikket_web/admin/__init__.py:71 +#: snikket_web/admin.py:71 msgid "New invitation link" msgstr "Neuer Einladungslink" -#: snikket_web/admin/__init__.py:104 +#: snikket_web/admin.py:104 msgid "Revoke" msgstr "Löschen" +#: snikket_web/main.py:33 +msgid "Address" +msgstr "Adresse" + +#: snikket_web/main.py:38 +msgid "Password" +msgstr "Passwort" + +#: snikket_web/main.py:60 +msgid "Invalid user name or password." +msgstr "Benutzername oder Passwort falsch." + +#: snikket_web/user.py:25 +msgid "Current password" +msgstr "Aktuelles Passwort" + +#: snikket_web/user.py:30 +msgid "New password" +msgstr "Neues Passwort" + +#: snikket_web/user.py:35 +msgid "Confirm new password" +msgstr "Neues Passwort (Bestätigung)" + +#: snikket_web/user.py:39 +msgid "The new passwords must match." +msgstr "Die neuen Passwörter müssen übereinstimmen." + +#: snikket_web/templates/admin_delete_user.html:12 +#: snikket_web/templates/admin_delete_user.html:16 +#: snikket_web/templates/admin_users.html:15 snikket_web/user.py:50 +msgid "Display name" +msgstr "Anzeigename" + +#: snikket_web/user.py:54 +msgid "Avatar" +msgstr "Bild" + +#: snikket_web/user.py:78 +msgid "Incorrect password" +msgstr "Ungültiges Passwort" + #: snikket_web/templates/admin_delete_user.html:4 #: snikket_web/templates/admin_users.html:29 #, python-format @@ -62,12 +92,6 @@ msgstr "Bist du sicher dass du den folgenden Benutzer löschen willst?" msgid "Login name" msgstr "Anmeldename" -#: snikket_web/templates/admin_delete_user.html:12 -#: snikket_web/templates/admin_delete_user.html:16 -#: snikket_web/templates/admin_users.html:15 snikket_web/user/__init__.py:48 -msgid "Display name" -msgstr "Anzeigename" - #: snikket_web/templates/admin_delete_user.html:14 #: snikket_web/templates/admin_users.html:16 msgid "Email address" @@ -287,30 +311,6 @@ msgstr "Profil" msgid "Apply" msgstr "Übernehmen" -#: snikket_web/user/__init__.py:23 -msgid "Current password" -msgstr "Aktuelles Passwort" - -#: snikket_web/user/__init__.py:28 -msgid "New password" -msgstr "Neues Passwort" - -#: snikket_web/user/__init__.py:33 -msgid "Confirm new password" -msgstr "Neues Passwort (Bestätigung)" - -#: snikket_web/user/__init__.py:37 -msgid "The new passwords must match." -msgstr "Die neuen Passwörter müssen übereinstimmen." - -#: snikket_web/user/__init__.py:52 -msgid "Avatar" -msgstr "Bild" - -#: snikket_web/user/__init__.py:76 -msgid "Incorrect password" -msgstr "Ungültiges Passwort" - #~ msgid "none" #~ msgstr "keiner" diff --git a/snikket_web/translations/en/LC_MESSAGES/messages.po b/snikket_web/translations/en/LC_MESSAGES/messages.po index 2d4d691..9624f53 100644 --- a/snikket_web/translations/en/LC_MESSAGES/messages.po +++ b/snikket_web/translations/en/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2021-01-17 20:09+0100\n" +"POT-Creation-Date: 2021-01-17 20:11+0100\n" "PO-Revision-Date: 2020-03-07 16:50+0100\n" "Last-Translator: FULL NAME \n" "Language: en\n" @@ -18,30 +18,60 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.9.0\n" -#: snikket_web/__init__.py:40 +#: snikket_web/admin.py:44 +msgid "Delete user permanently" +msgstr "" + +#: snikket_web/admin.py:71 +msgid "New invitation link" +msgstr "" + +#: snikket_web/admin.py:104 +msgid "Revoke" +msgstr "" + +#: snikket_web/main.py:33 msgid "Address" msgstr "Address" -#: snikket_web/__init__.py:45 +#: snikket_web/main.py:38 msgid "Password" msgstr "Password" -#: snikket_web/__init__.py:74 +#: snikket_web/main.py:60 #, fuzzy msgid "Invalid user name or password." msgstr "Confirm new password" -#: snikket_web/admin/__init__.py:44 -msgid "Delete user permanently" -msgstr "" +#: snikket_web/user.py:25 +msgid "Current password" +msgstr "Current password" -#: snikket_web/admin/__init__.py:71 -msgid "New invitation link" -msgstr "" +#: snikket_web/user.py:30 +msgid "New password" +msgstr "New password" -#: snikket_web/admin/__init__.py:104 -msgid "Revoke" -msgstr "" +#: snikket_web/user.py:35 +msgid "Confirm new password" +msgstr "Confirm new password" + +#: snikket_web/user.py:39 +msgid "The new passwords must match." +msgstr "The new passwords must match." + +#: snikket_web/templates/admin_delete_user.html:12 +#: snikket_web/templates/admin_delete_user.html:16 +#: snikket_web/templates/admin_users.html:15 snikket_web/user.py:50 +msgid "Display name" +msgstr "Display name" + +#: snikket_web/user.py:54 +msgid "Avatar" +msgstr "Avatar" + +#: snikket_web/user.py:78 +msgid "Incorrect password" +msgstr "Incorrect password" #: snikket_web/templates/admin_delete_user.html:4 #: snikket_web/templates/admin_users.html:29 @@ -63,12 +93,6 @@ msgstr "" msgid "Login name" msgstr "" -#: snikket_web/templates/admin_delete_user.html:12 -#: snikket_web/templates/admin_delete_user.html:16 -#: snikket_web/templates/admin_users.html:15 snikket_web/user/__init__.py:48 -msgid "Display name" -msgstr "Display name" - #: snikket_web/templates/admin_delete_user.html:14 #: snikket_web/templates/admin_users.html:16 #, fuzzy @@ -281,30 +305,6 @@ msgstr "Profile" msgid "Apply" msgstr "Apply" -#: snikket_web/user/__init__.py:23 -msgid "Current password" -msgstr "Current password" - -#: snikket_web/user/__init__.py:28 -msgid "New password" -msgstr "New password" - -#: snikket_web/user/__init__.py:33 -msgid "Confirm new password" -msgstr "Confirm new password" - -#: snikket_web/user/__init__.py:37 -msgid "The new passwords must match." -msgstr "The new passwords must match." - -#: snikket_web/user/__init__.py:52 -msgid "Avatar" -msgstr "Avatar" - -#: snikket_web/user/__init__.py:76 -msgid "Incorrect password" -msgstr "Incorrect password" - #~ msgid "none" #~ msgstr "" diff --git a/snikket_web/user/__init__.py b/snikket_web/user.py similarity index 86% rename from snikket_web/user/__init__.py rename to snikket_web/user.py index 9afe9dc..353f29d 100644 --- a/snikket_web/user/__init__.py +++ b/snikket_web/user.py @@ -1,24 +1,26 @@ import typing import quart.flask_patch - from quart import Blueprint, render_template, request, redirect, url_for import quart.exceptions -from flask_wtf import FlaskForm -from flask_babel import lazy_gettext as _l, _ + import wtforms -from snikket_web.prosodyclient import client +import flask_wtf -user_bp = Blueprint('user', __name__, url_prefix="/user") +from flask_babel import lazy_gettext as _l, _ + +from .infra import client + +bp = Blueprint('user', __name__) -@user_bp.context_processor +@bp.context_processor async def proc() -> typing.Mapping[str, typing.Any]: return {"user_info": await client.get_user_info()} -class ChangePasswordForm(FlaskForm): +class ChangePasswordForm(flask_wtf.FlaskForm): # type:ignore current_password = wtforms.PasswordField( _l("Current password"), validators=[wtforms.validators.InputRequired()] @@ -39,11 +41,11 @@ class ChangePasswordForm(FlaskForm): ) -class LogoutForm(FlaskForm): +class LogoutForm(flask_wtf.FlaskForm): # type:ignore pass -class ProfileForm(FlaskForm): +class ProfileForm(flask_wtf.FlaskForm): # type:ignore nickname = wtforms.TextField( _l("Display name"), ) @@ -53,14 +55,14 @@ class ProfileForm(FlaskForm): ) -@user_bp.route("/") +@bp.route("/") @client.require_session() async def index() -> str: user_info = await client.get_user_info() return await render_template("user_home.html", user_info=user_info) -@user_bp.route('/passwd', methods=["GET", "POST"]) +@bp.route('/passwd', methods=["GET", "POST"]) @client.require_session() async def change_pw() -> typing.Union[str, quart.Response]: form = ChangePasswordForm() @@ -81,7 +83,7 @@ async def change_pw() -> typing.Union[str, quart.Response]: return await render_template("user_passwd.html", form=form) -@user_bp.route("/profile", methods=["GET", "POST"]) +@bp.route("/profile", methods=["GET", "POST"]) @client.require_session() async def profile() -> typing.Union[str, quart.Response]: form = ProfileForm() @@ -106,12 +108,12 @@ async def profile() -> typing.Union[str, quart.Response]: return await render_template("user_profile.html", form=form) -@user_bp.route("/logout", methods=["GET", "POST"]) +@bp.route("/logout", methods=["GET", "POST"]) @client.require_session() async def logout() -> typing.Union[quart.Response, str]: form = LogoutForm() if form.validate_on_submit(): await client.logout() - return redirect(url_for("home")) + return redirect(url_for("main.home")) return await render_template("user_logout.html", form=form)