Compare commits

...

17 Commits

Author SHA1 Message Date
Jonas Schäfer
b36fc0d5ac Bump hsluv to 5.x
Fixes #134.
2022-05-30 17:38:18 +02:00
Jonas Schäfer
68f72743c5 Bump quart to version 0.17
This is needed because jinja2 had an update which caused the portal to
not work at all:

```
ImportError: cannot import name 'escape' from 'jinja2'
```

Quart needed updating for that.

This update required a lot of typefixes. Apparently, the "canned"
responses (like redirect) are now plain werkzeug responses, while
quart.Response does not inherit from werkzeug.Response (otherwise, we
could've changed the type annotations to werkzeug.Response everywhere,
but that doesn't work because a quart.Response is not a
werkzeug.Response).

P.S.: This time, I *did* check that avatar uploads don't break (see
b007afc).
2022-05-30 17:37:54 +02:00
Daniel Holmgaard
8741efb2c4 Translated using Weblate (Danish)
Currently translated at 100.0% (321 of 321 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/da/
2022-05-30 14:01:08 +00:00
Daniel Holmgaard
a0e8933b64 Translated using Weblate (Danish)
Currently translated at 95.6% (307 of 321 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/da/
2022-05-26 23:01:53 +00:00
Zack Zhou
edb3154127 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (321 of 321 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/zh_Hans/
2022-05-20 00:01:45 +00:00
Jonas Schäfer
c278d4ace9 Merge pull request #132 from Raka-loah/master
Add Simplified Chinese support
2022-05-15 08:14:08 +02:00
Raka-loah
bbfe8624ef Add Simplified Chinese support 2022-05-14 17:53:40 +08:00
David Baraniak
8bcf619cef Translated using Weblate (French)
Currently translated at 100.0% (321 of 321 strings)

Translation: Snikket/Web Portal
Translate-URL: http://i18n.sotecware.net/projects/snikket/web-portal/fr/
2022-04-11 13:00:44 +00:00
Matthew Wild
846a5e49fd Merge pull request #122 from snikket-im/feature/autocomplete-hints
Add autocomplete hints to password forms
2022-02-07 11:43:20 +00:00
Matthew Wild
b3ff7f04b5 Merge pull request #127 from snikket-im/fix/unhealthy
Install netcat in final image instead of build image
2022-02-07 11:42:40 +00:00
Jonas Schäfer
0ac4ab8142 Install netcat in final image instead of build image
`nc` (from netcat) is required for the healthcheck. In
c1cf6ab1e5, the installation was
erroneously moved to the builder image, instead of the final image, so
it was missing since then from the actual application image, causing it
to always show as unhealthy.

Fixes #126.
2022-02-05 13:51:30 +01:00
Matthew Wild
d4a38f5049 Merge pull request #125 from snikket-im/fix/support-requirements-compilation
Dockerfile: Add dev headers required for building deps
2022-02-01 09:09:50 +00:00
Matthew Wild
344a4d3e93 Dockerfile: Add dev headers required for building deps 2022-02-01 09:04:25 +00:00
Matthew Wild
57f1047526 Merge pull request #124 from snikket-im/fix/support-requirements-compilation
Dockerfile: Ensure a compiler is available while pip-installing requirements
2022-02-01 08:16:33 +00:00
Matthew Wild
b036caa85e Dockerfile: Ensure a compiler is available while pip-installing requirements
Dependencies are not necessarily packaged for all architectures. In some cases
(such as aiohttp, and others, on ARM) pip will attempt to compile the
dependency from scratch. Since switching to multi-stage builds, we have been
installing these without a compiler present which caused the build to fail on
ARM architectures.

This commit temporarily installs build-essential packages while running pip,
then removes them again afterwards.
2022-01-31 21:45:39 +00:00
Matthew Wild
08845cb9f0 Merge pull request #123 from snikket-im/hotfix/error-handling
Fix error handling when building image
2022-01-31 18:11:58 +00:00
Jonas Schäfer
4bd58c1104 Add autocomplete hints to password forms
This allows user agents to do smart things like filling in the current
password only where it makes sense or integrate nicely with a password
manager.

Fixes #94.
2022-01-22 15:34:27 +01:00
18 changed files with 1729 additions and 144 deletions

View File

@@ -6,8 +6,7 @@ RUN set -eu; \
apt-get install -y --no-install-recommends \
python3 python3-pip python3-setuptools python3-wheel \
libpython3-dev \
make build-essential \
netcat;
make build-essential;
COPY requirements.txt /opt/snikket-web-portal/requirements.txt
COPY build-requirements.txt /opt/snikket-web-portal/build-requirements.txt
@@ -34,21 +33,22 @@ ENV SNIKKET_WEB_PYENV=/etc/snikket-web-portal/env.py
ENV SNIKKET_WEB_PROSODY_ENDPOINT=http://127.0.0.1:5280/
HEALTHCHECK CMD nc -zv ${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE:-127.0.0.1} ${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT:-5765}
COPY requirements.txt /opt/snikket-web-portal/requirements.txt
WORKDIR /opt/snikket-web-portal
RUN set -eu; \
export DEBIAN_FRONTEND=noninteractive ; \
apt-get update ; \
apt-get install -y --no-install-recommends \
python3 python3-pip python3-setuptools python3-wheel; \
python3 python3-pip python3-setuptools python3-wheel build-essential libpython3-dev netcat; \
pip3 install -r requirements.txt; \
apt-get remove -y --autoremove build-essential libpython3-dev; \
apt-get clean ; rm -rf /var/lib/apt/lists; \
pip3 install hypercorn; \
rm -rf /root/.cache;
WORKDIR /opt/snikket-web-portal
COPY requirements.txt /opt/snikket-web-portal/requirements.txt
RUN set -eu; pip3 install -r requirements.txt; rm -rf /root/.cache;
HEALTHCHECK CMD nc -zv ${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE:-127.0.0.1} ${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT:-5765}
COPY --from=build /opt/snikket-web-portal/snikket_web/ /opt/snikket-web-portal/snikket_web
COPY babel.cfg /opt/snikket-web-portal/babel.cfg

View File

@@ -1,7 +1,7 @@
aiohttp~=3.6
quart~=0.11,<0.15
quart~=0.17
flask-wtf~=0.14
hsluv~=0.0.2
hsluv~=5.0
flask-babel~=1.0
email-validator~=1.1
environ-config~=20.0

View File

@@ -18,6 +18,8 @@ from quart import (
jsonify,
)
import werkzeug.exceptions
import environ
from . import colour, infra
@@ -40,7 +42,7 @@ async def proc() -> typing.Dict[str, typing.Any]:
try:
user_info = await infra.client.get_user_info()
except (aiohttp.ClientError, quart.exceptions.HTTPException):
except (aiohttp.ClientError, werkzeug.exceptions.HTTPException):
user_info = {}
return {
@@ -105,16 +107,16 @@ async def backend_error_handler(exc: Exception) -> quart.Response:
async def generic_http_error(
exc: quart.exceptions.HTTPException,
exc: werkzeug.exceptions.HTTPException,
) -> quart.Response:
return quart.Response(
await render_template(
"generic_http_error.html",
status=exc.status_code,
status=exc.code,
description=exc.description,
name=exc.name,
),
status=exc.status_code,
status=exc.code,
)
@@ -153,6 +155,7 @@ class AppConfig:
"it",
"pl",
"sv",
"zh_Hans_CN",
], converter=autosplit)
apple_store_url = environ.var(
"https://apps.apple.com/us/app/snikket/id1545164189",
@@ -199,19 +202,19 @@ def create_app() -> quart.Quart:
app.context_processor(proc)
app.register_error_handler(
aiohttp.ClientConnectorError,
backend_error_handler, # type:ignore
backend_error_handler,
)
app.register_error_handler(
quart.exceptions.HTTPException,
werkzeug.exceptions.HTTPException,
generic_http_error, # type:ignore
)
app.register_error_handler(
Exception,
generic_error_handler, # type:ignore
generic_error_handler,
)
@app.route("/")
async def index() -> quart.Response:
async def index() -> werkzeug.Response:
if infra.client.has_session:
return redirect(url_for('user.index'))

View File

@@ -7,6 +7,8 @@ from datetime import datetime
import aiohttp
import werkzeug.exceptions
import quart.flask_patch
import wtforms
@@ -92,7 +94,7 @@ class EditUserForm(BaseForm):
@bp.route("/user/<localpart>/", methods=["GET", "POST"])
@client.require_admin_session()
async def edit_user(localpart: str) -> typing.Union[quart.Response, str]:
async def edit_user(localpart: str) -> typing.Union[werkzeug.Response, str]:
target_user_info = await client.get_user_by_localpart(localpart)
form = EditUserForm()
@@ -147,7 +149,7 @@ class DeleteUserForm(BaseForm):
@bp.route("/user/<localpart>/delete", methods=["GET", "POST"])
@client.require_admin_session()
async def delete_user(localpart: str) -> typing.Union[str, quart.Response]:
async def delete_user(localpart: str) -> typing.Union[str, werkzeug.Response]:
target_user_info = await client.get_user_by_localpart(localpart)
form = DeleteUserForm()
if form.validate_on_submit():
@@ -186,7 +188,7 @@ async def debug_user(localpart: str) -> typing.Union[str, quart.Response]:
@client.require_admin_session()
async def user_password_reset_link(
id_: str,
) -> typing.Union[str, quart.Response]:
) -> typing.Union[str, werkzeug.Response]:
invite_info = await client.get_invite_by_id(
id_,
)
@@ -278,7 +280,7 @@ class InvitePost(BaseForm):
@bp.route("/invitations", methods=["GET", "POST"])
@client.require_admin_session()
async def invitations() -> typing.Union[str, quart.Response]:
async def invitations() -> typing.Union[str, werkzeug.Response]:
invites = sorted(
(
invite
@@ -324,7 +326,7 @@ class InviteForm(BaseForm):
@bp.route("/invitation/-/new", methods=["POST"])
@client.require_admin_session()
async def create_invite() -> typing.Union[str, quart.Response]:
async def create_invite() -> typing.Union[str, werkzeug.Response]:
form = InvitePost()
circles = await client.list_groups()
form.circles.choices = [
@@ -352,7 +354,7 @@ async def create_invite() -> typing.Union[str, quart.Response]:
@bp.route("/invitation/<id_>", methods=["GET", "POST"])
@client.require_admin_session()
async def edit_invite(id_: str) -> typing.Union[str, quart.Response]:
async def edit_invite(id_: str) -> typing.Union[str, werkzeug.Response]:
try:
invite_info = await client.get_invite_by_id(id_)
except aiohttp.ClientResponseError as exc:
@@ -418,7 +420,7 @@ async def circles() -> str:
@bp.route("/circle/-/new", methods=["POST"])
@client.require_admin_session()
async def create_circle() -> typing.Union[str, quart.Response]:
async def create_circle() -> typing.Union[str, werkzeug.Response]:
create_form = CirclePost()
if create_form.validate_on_submit():
circle = await client.create_group(
@@ -464,7 +466,7 @@ class EditCircleForm(BaseForm):
@bp.route("/circle/<id_>", methods=["GET", "POST"])
@client.require_admin_session()
async def edit_circle(id_: str) -> typing.Union[str, quart.Response]:
async def edit_circle(id_: str) -> typing.Union[str, werkzeug.Response]:
async with client.authenticated_session() as session:
try:
circle = await client.get_group_by_id(
@@ -626,7 +628,7 @@ class AnnouncementForm(BaseForm):
@bp.route("/system/", methods=["GET", "POST"])
@client.require_admin_session()
async def system() -> typing.Union[str, quart.Response]:
async def system() -> typing.Union[str, werkzeug.Response]:
form = AnnouncementForm()
if form.validate_on_submit():
@@ -657,7 +659,7 @@ async def system() -> typing.Union[str, quart.Response]:
now = time.time()
try:
prosody_metrics = await client.get_system_metrics()
except quart.exceptions.NotFound:
except werkzeug.exceptions.NotFound:
# server does not offer the endpoint for whatever reason -- ignore
prosody_metrics = {}

View File

@@ -15,6 +15,8 @@ from quart import (
session as http_session,
)
import werkzeug
import wtforms
from flask_babel import lazy_gettext as _l, gettext
@@ -46,14 +48,14 @@ def apple_store_badge() -> str:
@bp.context_processor
def context() -> typing.Mapping[str, typing.Any]:
def context() -> typing.Dict[str, typing.Any]:
return {
"apple_store_badge": apple_store_badge,
}
@bp.route("/<id_>")
async def view_old(id_: str) -> quart.Response:
async def view_old(id_: str) -> werkzeug.Response:
return redirect(url_for(".view", id_=id_))
@@ -131,7 +133,7 @@ class RegisterForm(BaseForm):
@bp.route("/<id_>/register", methods=["GET", "POST"])
async def register(id_: str) -> typing.Union[str, quart.Response]:
async def register(id_: str) -> typing.Union[str, werkzeug.Response]:
try:
invite = await client.get_public_invite_by_id(id_)
except aiohttp.ClientResponseError as exc:
@@ -199,7 +201,7 @@ class ResetForm(BaseForm):
@bp.route("/<id_>/reset", methods=["GET", "POST"])
async def reset(id_: str) -> typing.Union[str, quart.Response]:
async def reset(id_: str) -> typing.Union[str, werkzeug.Response]:
try:
invite = await client.get_public_invite_by_id(id_)
except aiohttp.ClientResponseError as exc:
@@ -300,5 +302,5 @@ async def reset_success() -> str:
@bp.route("/-")
async def index() -> quart.Response:
async def index() -> werkzeug.Response:
return redirect(url_for("index"))

View File

@@ -18,6 +18,8 @@ from quart import (
flash,
)
import werkzeug.exceptions
import babel
import wtforms
@@ -48,7 +50,7 @@ class LoginForm(BaseForm):
@bp.route("/-")
async def index() -> quart.Response:
async def index() -> werkzeug.Response:
return redirect(url_for("index"))
@@ -56,7 +58,7 @@ ERR_CREDENTIALS_INVALID = _l("Invalid username or password.")
@bp.route("/login", methods=["GET", "POST"])
async def login() -> typing.Union[str, quart.Response]:
async def login() -> typing.Union[str, werkzeug.Response]:
if client.has_session and (await client.test_session()):
return redirect(url_for('user.index'))
@@ -76,7 +78,7 @@ async def login() -> typing.Union[str, quart.Response]:
password = form.password.data
try:
await client.login(jid, password)
except quart.exceptions.Unauthorized:
except werkzeug.exceptions.Unauthorized:
form.password.errors.append(ERR_CREDENTIALS_INVALID)
else:
await flash(
@@ -95,14 +97,13 @@ async def about() -> str:
if current_app.debug or client.is_admin_session:
version = _version.version
extra_versions["Quart"] = quart.__version__
extra_versions["aiohttp"] = aiohttp.__version__
extra_versions["babel"] = babel.__version__
extra_versions["wtforms"] = wtforms.__version__
extra_versions["flask-wtf"] = flask_wtf.__version__
try:
extra_versions["Prosody"] = await client.get_server_version()
except quart.exceptions.Unauthorized:
except werkzeug.exceptions.Unauthorized:
extra_versions["Prosody"] = "unknown"
return await render_template(

View File

@@ -19,7 +19,9 @@ from quart import (
current_app, _app_ctx_stack, session as http_session, abort, redirect,
url_for,
)
import quart.exceptions
import quart
import werkzeug.exceptions
from . import xmpputil
from .xmpputil import split_jid
@@ -386,16 +388,16 @@ class ProsodyClient:
) -> typing.Callable[
[typing.Callable[..., typing.Awaitable[T]]],
typing.Callable[..., typing.Awaitable[
typing.Union[T, quart.Response]]]]:
typing.Union[T, quart.Response, werkzeug.Response]]]]:
def decorator(
f: typing.Callable[..., typing.Awaitable[T]],
) -> typing.Callable[..., typing.Awaitable[
typing.Union[T, quart.Response]]]:
typing.Union[T, quart.Response, werkzeug.Response]]]:
@functools.wraps(f)
async def wrapped(
*args: typing.Any,
**kwargs: typing.Any,
) -> typing.Union[T, quart.Response]:
) -> typing.Union[T, quart.Response, werkzeug.Response]:
if not self.has_session or not (await self.test_session()):
redirect_to_value = redirect_to
if redirect_to_value is not False:
@@ -415,17 +417,17 @@ class ProsodyClient:
) -> typing.Callable[
[typing.Callable[..., typing.Awaitable[T]]],
typing.Callable[..., typing.Awaitable[
typing.Union[T, quart.Response]]]]:
typing.Union[T, quart.Response, werkzeug.Response]]]]:
def decorator(
f: typing.Callable[..., typing.Awaitable[T]],
) -> typing.Callable[..., typing.Awaitable[
typing.Union[T, quart.Response]]]:
typing.Union[T, quart.Response, werkzeug.Response]]]:
@functools.wraps(f)
@self.require_session(redirect_to=redirect_to)
async def wrapped(
*args: typing.Any,
**kwargs: typing.Any,
) -> typing.Union[T, quart.Response]:
) -> typing.Union[T, quart.Response, werkzeug.Response]:
if not self.is_admin_session:
raise abort(403, "This is not for you.")
@@ -492,7 +494,7 @@ class ProsodyClient:
session=session,
)
avatar_hash = avatar_info["sha1"]
except quart.exceptions.HTTPException:
except werkzeug.exceptions.HTTPException:
avatar_hash = None
return {
@@ -644,7 +646,7 @@ class ProsodyClient:
new_access_model,
)
))
except quart.exceptions.NotFound:
except werkzeug.exceptions.NotFound:
if ignore_not_found:
return
raise
@@ -774,7 +776,7 @@ class ProsodyClient:
session: aiohttp.ClientSession,
) -> str:
access_models = filter(
lambda x: not isinstance(x, quart.exceptions.NotFound),
lambda x: not isinstance(x, werkzeug.exceptions.NotFound),
await asyncio.gather(
self.get_avatar_access_model(session=session),
self.get_nickname_access_model(session=session),

View File

@@ -28,12 +28,12 @@
</div>
<div class="f-ebox">
{{ form.password.label }}
{{ form.password }}
{{ form.password(autocomplete="new-password") }}
<p class="field-desc weak">{% trans %}Enter a secure password that you do not use anywhere else.{% endtrans %}</p>
</div>
<div class="f-ebox">
{{ form.password_confirm.label }}
{{ form.password_confirm }}
{{ form.password_confirm(autocomplete="new-password") }}
</div>
<div class="f-bbox">
{%- call form_button("done", form.action_register, class="primary") -%}{%- endcall -%}

View File

@@ -17,11 +17,11 @@
{%- call render_errors(form) %}{% endcall -%}
<div class="f-ebox">
{{ form.password.label }}
{{ form.password }}
{{ form.password(autocomplete="new-password") }}
</div>
<div class="f-ebox">
{{ form.password_confirm.label }}
{{ form.password_confirm }}
{{ form.password_confirm(autocomplete="new-password") }}
</div>
<div class="f-bbox">
{%- call form_button("passwd", form.action_reset, class="primary") -%}{%- endcall -%}

View File

@@ -9,15 +9,15 @@
{%- endcall -%}
<div class="f-ebox">
{{ form.current_password.label(class="required") }}
{{ form.current_password(class=("has-error" if form.current_password.name in form.errors else "")) }}
{{ form.current_password(class=("has-error" if form.current_password.name in form.errors else ""), autocomplete="current-password") }}
</div>
<div class="f-ebox">
{{ form.new_password.label(class="required") }}
{{ form.new_password }}
{{ form.new_password(autocomplete="new-password") }}
</div>
<div class="f-ebox">
{{ form.new_password_confirm.label(class="required") }}
{{ form.new_password_confirm(class=("has-error" if form.new_password_confirm.name in form.errors else "")) }}
{{ form.new_password_confirm(class=("has-error" if form.new_password_confirm.name in form.errors else ""), autocomplete="new-password") }}
</div>
<div class="box warning">
<header>{% trans %}Warning{% endtrans %}</header>

View File

@@ -6,18 +6,18 @@
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"Report-Msgid-Bugs-To: translations@snikket.org\n"
"POT-Creation-Date: 2022-01-17 17:27+0100\n"
"PO-Revision-Date: 2021-04-02 19:01+0000\n"
"Last-Translator: Daniel Holmgaard <annoncer@protonmail.com>\n"
"Language-Team: Danish <https://i18n.sotecware.net/projects/snikket/web-"
"portal/da/>\n"
"PO-Revision-Date: 2022-05-30 14:01+0000\n"
"Last-Translator: Daniel Holmgaard <fovatis@tutanota.com>\n"
"Language-Team: Danish <http://i18n.sotecware.net/projects/snikket/web-portal/"
"da/>\n"
"Language: da\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.5.1\n"
"X-Generator: Weblate 4.8.1\n"
"Generated-By: Babel 2.9.0\n"
#: snikket_web/admin.py:68 snikket_web/templates/admin_delete_user.html:10
@@ -194,23 +194,23 @@ msgstr "Bruger fjernet fra cirkel"
#: snikket_web/admin.py:609
msgid "Message contents"
msgstr ""
msgstr "Meddelelsens indhold"
#: snikket_web/admin.py:615
msgid "Only send to online users"
msgstr ""
msgstr "Send kun til online brugere"
#: snikket_web/admin.py:619
msgid "Post to all users"
msgstr ""
msgstr "Send til alle brugere"
#: snikket_web/admin.py:623
msgid "Send preview to yourself"
msgstr ""
msgstr "Send forhåndsvisning til dig selv"
#: snikket_web/admin.py:645
msgid "Announcement sent!"
msgstr ""
msgstr "Bekendgørelse sendt!"
#: snikket_web/infra.py:51
msgid "Main"
@@ -221,6 +221,8 @@ msgid ""
"The account data you tried to import is too large to upload. Please contact "
"your Snikket operator."
msgstr ""
"De kontodata, du forsøgte at importere, er for store til at uploade. Kontakt "
"din Snikket-operatør."
#: snikket_web/invite.py:112
msgid "Username"
@@ -261,11 +263,11 @@ msgstr "Ændr adgangskode"
#: snikket_web/invite.py:244
msgid "Account data file"
msgstr ""
msgstr "Kontodatafil"
#: snikket_web/invite.py:248
msgid "Import data"
msgstr ""
msgstr "Importer data"
#: snikket_web/invite.py:269
#, python-format
@@ -273,6 +275,8 @@ msgid ""
"The account data you tried to import is in an unknown format. Please upload "
"an XML file in XEP-0227 format (provided format: %(mimetype)s)."
msgstr ""
"De kontodata, du forsøgte at importere, er i et ukendt format. Upload en XML-"
"fil i XEP-0227-format (forudsat format: %(mimetype)s)."
#: snikket_web/invite.py:289 snikket_web/templates/unauth.html:18
#: snikket_web/user.py:178
@@ -341,11 +345,11 @@ msgstr "Opdater profil"
#: snikket_web/user.py:82
msgid "Account data"
msgstr ""
msgstr "Kontodata"
#: snikket_web/user.py:86
msgid "Upload"
msgstr ""
msgstr "Upload"
#: snikket_web/user.py:111
msgid "Incorrect password."
@@ -369,11 +373,11 @@ msgstr "Profil opdateret"
#: snikket_web/user.py:184
msgid "Export"
msgstr ""
msgstr "Exporter"
#: snikket_web/user.py:202
msgid "You currently have no account data to export."
msgstr ""
msgstr "Du har i øjeblikket ingen kontodata at eksportere."
#: snikket_web/templates/_footer.html:4
#, python-format
@@ -662,7 +666,7 @@ msgstr "Cirkel medlemmer"
#: snikket_web/templates/admin_edit_circle.html:71
msgid "The user has been deleted from the server."
msgstr ""
msgstr "Brugeren er blevet slettet fra serveren."
#: snikket_web/templates/admin_edit_circle.html:71
#: snikket_web/templates/library.j2:108
@@ -876,22 +880,20 @@ msgstr "Håndter invitationer"
#: snikket_web/templates/admin_home.html:35
msgid "System health"
msgstr ""
msgstr "Systemets sundhed"
#: snikket_web/templates/admin_home.html:38
msgid "View the server status or send a broadcast message to all users."
msgstr ""
msgstr "Vis serverstatus, eller send en udsendelsesmeddelelse til alle brugere."
#: snikket_web/templates/admin_home.html:40
msgid "Send a broadcast message to all users."
msgstr ""
msgstr "Send en udsendelsesmeddelelse til alle brugere."
#: snikket_web/templates/admin_home.html:43
#: snikket_web/templates/admin_system.html:4
#, fuzzy
#| msgid "Manage users"
msgid "Manage system"
msgstr "Håndter brugere"
msgstr "Håndter system"
#: snikket_web/templates/admin_home.html:48
msgid "Go back to your user's web portal page."
@@ -952,11 +954,11 @@ msgstr "Ødelæg link"
#: snikket_web/templates/admin_system.html:6
msgid "Overall system status"
msgstr ""
msgstr "Samlet systemstatus"
#: snikket_web/templates/admin_system.html:9
msgid "System load (5 minute average)"
msgstr ""
msgstr "Systembelastning (5 minutters gennemsnit)"
#: snikket_web/templates/admin_system.html:14
#: snikket_web/templates/admin_system.html:22
@@ -967,11 +969,11 @@ msgstr ""
#: snikket_web/templates/admin_system.html:76
#: snikket_web/templates/admin_system.html:84
msgid "unknown"
msgstr ""
msgstr "ukendt"
#: snikket_web/templates/admin_system.html:17
msgid "Memory use"
msgstr ""
msgstr "Forbrug af hukommelse"
#: snikket_web/templates/admin_system.html:20
#, python-format
@@ -979,54 +981,56 @@ msgid ""
"%(percentage_global)s of %(mem_available)s. Of that, Snikket uses "
"%(percentage_snikket)s."
msgstr ""
"%(percentage_global)s af %(mem_available)s. Der af bruger Snikket "
"%(percentage_snikket)s."
#: snikket_web/templates/admin_system.html:27
msgid "Web portal status"
msgstr ""
msgstr "Webportalens status"
#: snikket_web/templates/admin_system.html:30
#: snikket_web/templates/admin_system.html:53
msgid "Version"
msgstr ""
msgstr "Version"
#: snikket_web/templates/admin_system.html:31
#: snikket_web/templates/admin_system.html:54
msgid "View all versions"
msgstr ""
msgstr "Vis alle versioner"
#: snikket_web/templates/admin_system.html:32
#: snikket_web/templates/admin_system.html:55
msgid "Average CPU use"
msgstr ""
msgstr "Gennemsnitlig CPU-forbrug"
#: snikket_web/templates/admin_system.html:40
#: snikket_web/templates/admin_system.html:63
msgid "Current memory use"
msgstr ""
msgstr "Nuværende hukommelsesbrug"
#: snikket_web/templates/admin_system.html:50
#, fuzzy
#| msgid "Snikket Web Portal"
msgid "Snikket server status"
msgstr "Snikket Webportal"
msgstr "Snikket server status"
#: snikket_web/templates/admin_system.html:71
msgid "Storage used by shared files"
msgstr ""
msgstr "Lagerplads, der bruges af delte filer"
#: snikket_web/templates/admin_system.html:79
msgid "Connected devices"
msgstr ""
msgstr "Forbundet enheder"
#: snikket_web/templates/admin_system.html:90
msgid "Broadcast message"
msgstr ""
msgstr "Send besked"
#: snikket_web/templates/admin_system.html:92
msgid ""
"This form allows you to send a message to all users currently online on your "
"Snikket server. Use it wisely."
msgstr ""
"Denne formular giver dig mulighed for at sende en besked til alle brugere, "
"der i øjeblikket er online på din Snikket-server. Brug den med omtanke."
#: snikket_web/templates/admin_users.html:19
msgid "The user is an administrator."
@@ -1300,22 +1304,20 @@ msgid ""
"You can now safely close this page, or log in to the web portal to <a href="
"\"%(login_url)s\">manage your account</a>."
msgstr ""
"Du kan nu trygt lukke denne side eller logge ind på webportalen for at <a "
"href=\"%(login_url)s\">administrere din konto</a>."
#: snikket_web/templates/invite_success.html:21
#, fuzzy
#| msgid "Operation successful"
msgid "Import successful"
msgstr "Operation lykkes"
msgstr "Importering lykkes"
#: snikket_web/templates/invite_success.html:22
msgid "Congratulations! Your account data has been successfully imported."
msgstr ""
msgstr "Tillykke! Dine kontodata er blevet importeret."
#: snikket_web/templates/invite_success.html:26
#, fuzzy
#| msgid "Using the Snikket app"
msgid "Moving to Snikket?"
msgstr "Bruger Snikket appen"
msgstr "Flytte til Snikket?"
#: snikket_web/templates/invite_success.html:27
msgid ""
@@ -1324,10 +1326,14 @@ msgid ""
"information, etc.) from your previous account. When you have exported the "
"data from your previous account, upload it using the form below."
msgstr ""
"Hvis du flytter fra en anden Snikket-platform eller en anden XMPP-kompatibel "
"tjeneste, kan du eventuelt importere dataene (kontakter, profiloplysninger "
"osv.) fra din tidligere konto. Når du har eksporteret dataene fra din "
"tidligere konto, skal du uploade dem ved hjælp af nedenstående formular."
#: snikket_web/templates/invite_success.html:30
msgid "Upload account data"
msgstr ""
msgstr "Upload kontodata"
#: snikket_web/templates/invite_view.html:6
#, python-format
@@ -1558,10 +1564,8 @@ msgstr "Rediger profil"
#: snikket_web/templates/user_home.html:33
#: snikket_web/templates/user_manage_data.html:4
#, fuzzy
#| msgid "Manage users"
msgid "Manage your data"
msgstr "Håndter brugere"
msgstr "Håndter dine data"
#: snikket_web/templates/user_home.html:39
msgid "Your Snikket"
@@ -1588,16 +1592,16 @@ msgstr ""
"af de forbundne enheder."
#: snikket_web/templates/user_manage_data.html:8
#, fuzzy
#| msgid "Your account"
msgid "Export account"
msgstr "Din konto"
msgstr "Eksporter konto"
#: snikket_web/templates/user_manage_data.html:9
msgid ""
"Download your account data as a file for backup purposes or to move your "
"account to another service."
msgstr ""
"Download dine kontodata som en fil til sikkerhedskopieringsformål eller for "
"at flytte din konto til en anden tjeneste."
#: snikket_web/templates/user_passwd.html:5
msgid "Change your password"

View File

@@ -6,18 +6,18 @@
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"Report-Msgid-Bugs-To: translations@snikket.org\n"
"POT-Creation-Date: 2022-01-17 17:27+0100\n"
"PO-Revision-Date: 2021-06-19 15:01+0000\n"
"Last-Translator: Link Mauve <linkmauve@linkmauve.fr>\n"
"Language-Team: French <https://i18n.sotecware.net/projects/snikket/web-"
"portal/fr/>\n"
"PO-Revision-Date: 2022-04-11 13:00+0000\n"
"Last-Translator: David Baraniak <admin@chipmnk.dev>\n"
"Language-Team: French <http://i18n.sotecware.net/projects/snikket/web-portal/"
"fr/>\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 4.5.1\n"
"X-Generator: Weblate 4.8.1\n"
"Generated-By: Babel 2.9.0\n"
#: snikket_web/admin.py:68 snikket_web/templates/admin_delete_user.html:10
@@ -221,6 +221,8 @@ msgid ""
"The account data you tried to import is too large to upload. Please contact "
"your Snikket operator."
msgstr ""
"Les données du compte que vous avez essayé d'importer sont trop volumineuses "
"pour être téléchargées. Veuillez contacter votre opérateur Snikket."
#: snikket_web/invite.py:112
msgid "Username"
@@ -261,11 +263,11 @@ msgstr "Changer de mot de passe"
#: snikket_web/invite.py:244
msgid "Account data file"
msgstr ""
msgstr "Fichier de données du compte"
#: snikket_web/invite.py:248
msgid "Import data"
msgstr ""
msgstr "Importer les données"
#: snikket_web/invite.py:269
#, python-format
@@ -273,6 +275,9 @@ msgid ""
"The account data you tried to import is in an unknown format. Please upload "
"an XML file in XEP-0227 format (provided format: %(mimetype)s)."
msgstr ""
"Les données du compte que vous avez essayé d'importer sont dans un format "
"inconnu. Veuillez télécharger un fichier XML au format XEP-0227 (format "
"fourni : %(mimetype)s)."
#: snikket_web/invite.py:289 snikket_web/templates/unauth.html:18
#: snikket_web/user.py:178
@@ -341,11 +346,11 @@ msgstr "Mettre à jour le profil"
#: snikket_web/user.py:82
msgid "Account data"
msgstr ""
msgstr "Données du compte"
#: snikket_web/user.py:86
msgid "Upload"
msgstr ""
msgstr "Télécharger"
#: snikket_web/user.py:111
msgid "Incorrect password."
@@ -369,11 +374,11 @@ msgstr "Profil mis à jour"
#: snikket_web/user.py:184
msgid "Export"
msgstr ""
msgstr "Exporter"
#: snikket_web/user.py:202
msgid "You currently have no account data to export."
msgstr ""
msgstr "Vous n'avez actuellement aucune donnée de compte à exporter."
#: snikket_web/templates/_footer.html:4
#, python-format
@@ -1014,7 +1019,7 @@ msgstr "Statut du serveur Snikket"
#: snikket_web/templates/admin_system.html:71
msgid "Storage used by shared files"
msgstr ""
msgstr "Stockage utilisé par les fichiers partagés"
#: snikket_web/templates/admin_system.html:79
msgid "Connected devices"
@@ -1314,22 +1319,22 @@ msgid ""
"You can now safely close this page, or log in to the web portal to <a href="
"\"%(login_url)s\">manage your account</a>."
msgstr ""
"Vous pouvez maintenant fermer cette page en toute sécurité, ou vous "
"connecter au portail web pour <a href=\"%(login_url)s\">gérer votre "
"compte</a>."
#: snikket_web/templates/invite_success.html:21
#, fuzzy
#| msgid "Operation successful"
msgid "Import successful"
msgstr "Opération réussie"
msgstr "Importation réussie"
#: snikket_web/templates/invite_success.html:22
msgid "Congratulations! Your account data has been successfully imported."
msgstr ""
"Félicitations ! Les données de votre compte ont été importées avec succès."
#: snikket_web/templates/invite_success.html:26
#, fuzzy
#| msgid "Using the Snikket app"
msgid "Moving to Snikket?"
msgstr "En utilisant lapplication Snikket"
msgstr "Nouveau utilisateur Snikket ?"
#: snikket_web/templates/invite_success.html:27
msgid ""
@@ -1338,10 +1343,15 @@ msgid ""
"information, etc.) from your previous account. When you have exported the "
"data from your previous account, upload it using the form below."
msgstr ""
"Si vous passez d'une autre instance de Snikket ou d'un autre service "
"compatible XMPP, vous pouvez éventuellement importer les données (contacts, "
"informations de profil, etc.) de votre ancien compte. Lorsque vous avez "
"exporté les données de votre ancien compte, téléchargez-les en utilisant le "
"formulaire ci-dessous."
#: snikket_web/templates/invite_success.html:30
msgid "Upload account data"
msgstr ""
msgstr "Télécharger les données du compte"
#: snikket_web/templates/invite_view.html:6
#, python-format
@@ -1396,10 +1406,9 @@ msgstr "Télécharger sur lApp Store"
#: snikket_web/templates/invite_view.html:32
msgid "Get it on F-Droid"
msgstr ""
msgstr "Obtenez-le sur F-Droid"
#: snikket_web/templates/invite_view.html:35
#, fuzzy
msgid "Send to mobile device"
msgstr "Envoyer vers l'appareil"
@@ -1478,10 +1487,14 @@ 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 ""
"Après avoir téléchargé Snikket depuis l'App Store, vous devez revenir à ce "
"lien d'invitation et cliquer sur \"Ouvrir l'application\" pour continuer."
#: snikket_web/templates/invite_view.html:101
msgid "First download Snikket from the App Store using the button below:"
msgstr ""
"Téléchargez d'abord Snikket depuis l'App Store en utilisant le bouton ci-"
"dessous :"
#: snikket_web/templates/invite_view.html:103
#: snikket_web/templates/invite_view.html:131
@@ -1489,6 +1502,9 @@ 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 ""
"Une fois l'installation terminée, vous pouvez revenir à cette page et "
"appuyer sur le bouton \"Ouvrir l'application\" pour poursuivre la "
"configuration :"
#: snikket_web/templates/invite_view.html:121
#: snikket_web/templates/invite_view.html:130
@@ -1500,10 +1516,13 @@ msgid ""
"After installing Snikket via F-Droid, you have to return to this invite link "
"and tap on \"Open the app\" to proceed."
msgstr ""
"Après avoir installé Snikket via F-Droid, vous devez revenir à ce lien "
"d'invitation et appuyer sur \"Ouvrir l'application\" pour continuer."
#: snikket_web/templates/invite_view.html:129
msgid "First install Snikket from F-Droid using the button below:"
msgstr ""
"Installez d'abord Snikket depuis F-Droid en utilisant le bouton ci-dessous :"
#: snikket_web/templates/library.j2:18
msgid "Copy link"
@@ -1570,10 +1589,8 @@ msgstr "Éditer votre profil"
#: snikket_web/templates/user_home.html:33
#: snikket_web/templates/user_manage_data.html:4
#, fuzzy
#| msgid "Manage users"
msgid "Manage your data"
msgstr "Gérer les utilisateurs"
msgstr "Gérer vos données"
#: snikket_web/templates/user_home.html:39
msgid "Your Snikket"
@@ -1601,16 +1618,16 @@ msgstr ""
"autres appareils connectés."
#: snikket_web/templates/user_manage_data.html:8
#, fuzzy
#| msgid "Your account"
msgid "Export account"
msgstr "Votre compte"
msgstr "Exportation du compte"
#: snikket_web/templates/user_manage_data.html:9
msgid ""
"Download your account data as a file for backup purposes or to move your "
"account to another service."
msgstr ""
"Téléchargez les données de votre compte sous forme d'un fichier à des fins "
"de sauvegarde ou pour transférer votre compte vers un autre service."
#: snikket_web/templates/user_passwd.html:5
msgid "Change your password"

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@ from quart import (
flash,
current_app,
)
import quart.exceptions
import werkzeug.exceptions
import wtforms
@@ -96,7 +96,7 @@ async def index() -> str:
@bp.route('/passwd', methods=["GET", "POST"])
@client.require_session()
async def change_pw() -> typing.Union[str, quart.Response]:
async def change_pw() -> typing.Union[str, werkzeug.Response]:
form = ChangePasswordForm()
if form.validate_on_submit():
try:
@@ -104,8 +104,8 @@ async def change_pw() -> typing.Union[str, quart.Response]:
form.current_password.data,
form.new_password.data,
)
except (quart.exceptions.Unauthorized,
quart.exceptions.Forbidden):
except (werkzeug.exceptions.Unauthorized,
werkzeug.exceptions.Forbidden):
# server refused current password, set an appropriate error
form.current_password.errors.append(
_("Incorrect password."),
@@ -128,7 +128,7 @@ EAVATARTOOBIG = _l(
@bp.route("/profile", methods=["GET", "POST"])
@client.require_session()
async def profile() -> typing.Union[str, quart.Response]:
async def profile() -> typing.Union[str, werkzeug.Response]:
max_avatar_size = current_app.config["MAX_AVATAR_SIZE"]
form = ProfileForm()
@@ -221,7 +221,7 @@ async def manage_data() -> typing.Union[str, quart.Response]:
@bp.route("/logout", methods=["GET", "POST"])
@client.require_session()
async def logout() -> typing.Union[quart.Response, str]:
async def logout() -> typing.Union[werkzeug.Response, str]:
form = LogoutForm()
if form.validate_on_submit():
await client.logout()

View File

@@ -4,7 +4,7 @@ import typing
import xml.etree.ElementTree as ET
from quart import abort
import quart.exceptions
import werkzeug.exceptions
TAG_XMPP_ERROR = "error"
@@ -239,7 +239,7 @@ def extract_pubsub_item_get_reply(
) -> typing.Optional[ET.Element]:
try:
pubsub = extract_iq_reply(iq_tree, TAG_PUBSUB)
except quart.exceptions.NotFound:
except werkzeug.exceptions.NotFound:
return None
if pubsub is None: