You've already forked snikket-web-portal
Compare commits
154 Commits
feature/in
...
fix/invite
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40562d16f6 | ||
|
|
48a4a8f587 | ||
|
|
664112bf53 | ||
|
|
2dfc39e757 | ||
|
|
31b743a97f | ||
|
|
14a335bb06 | ||
|
|
6c8c213a88 | ||
|
|
2e224d96ce | ||
|
|
b70cb57497 | ||
|
|
124e0ce145 | ||
|
|
f2c79044e0 | ||
|
|
13bc4bb227 | ||
|
|
f1351eb5cc | ||
|
|
41573569af | ||
|
|
b1f3026b8a | ||
|
|
6794314a59 | ||
|
|
077e957a00 | ||
|
|
4902941145 | ||
|
|
5222c8eafe | ||
|
|
03ca7ac5bb | ||
|
|
56cee8bab6 | ||
|
|
b36fc0d5ac | ||
|
|
68f72743c5 | ||
|
|
8741efb2c4 | ||
|
|
a0e8933b64 | ||
|
|
edb3154127 | ||
|
|
eb22688302 | ||
|
|
c278d4ace9 | ||
|
|
bbfe8624ef | ||
|
|
8bcf619cef | ||
|
|
73fda3d623 | ||
|
|
846a5e49fd | ||
|
|
b3ff7f04b5 | ||
|
|
0ac4ab8142 | ||
|
|
d4a38f5049 | ||
|
|
344a4d3e93 | ||
|
|
57f1047526 | ||
|
|
b036caa85e | ||
|
|
08845cb9f0 | ||
|
|
6aa6e12680 | ||
|
|
4bd58c1104 | ||
|
|
a998348804 | ||
|
|
20abe4b903 | ||
|
|
a1ecb4ce80 | ||
|
|
b84b84b394 | ||
|
|
4f7a4fb5d4 | ||
|
|
6d50b1c2c7 | ||
|
|
34a23f8505 | ||
|
|
ebcb083b6a | ||
|
|
2f0b38b149 | ||
|
|
6244ad5c8a | ||
|
|
07fa1f0abd | ||
|
|
3d22458f9b | ||
|
|
3b768fe220 | ||
|
|
19cf82e894 | ||
|
|
fe0316708b | ||
|
|
81b0a58dc9 | ||
|
|
08aea153f9 | ||
|
|
958b3365f7 | ||
|
|
05caf38d37 | ||
|
|
390ecded42 | ||
|
|
f6395d4d9c | ||
|
|
32179c72cd | ||
|
|
3cb8185b1a | ||
|
|
481379d03f | ||
|
|
275b302531 | ||
|
|
e18f727db0 | ||
|
|
f7429413cd | ||
|
|
d5a46b69a6 | ||
|
|
51f2ebbd13 | ||
|
|
b4e6ee8943 | ||
|
|
52d8047546 | ||
|
|
aed9ad1cde | ||
|
|
b545c137b1 | ||
|
|
47642dc384 | ||
|
|
5d7183a0b8 | ||
|
|
c1cf6ab1e5 | ||
|
|
aee53a2e1a | ||
|
|
3a81a0140b | ||
|
|
5b4d4ddd36 | ||
|
|
28ff19c19c | ||
|
|
8e3837f704 | ||
|
|
4af78f635e | ||
|
|
98e7de3166 | ||
|
|
61c71b2145 | ||
|
|
6b35e9a259 | ||
|
|
58c2112fec | ||
|
|
c856afee82 | ||
|
|
c8356a8e9e | ||
|
|
0eb464f428 | ||
|
|
2a6ef3c8f1 | ||
|
|
b5d148458a | ||
|
|
261758b07a | ||
|
|
ff99c9488a | ||
|
|
fe78631039 | ||
|
|
12ddd288bf | ||
|
|
633fb0d084 | ||
|
|
f9690063bc | ||
|
|
65ed50acd3 | ||
|
|
aa04320d70 | ||
|
|
818d50a1bb | ||
|
|
c7ba7985ea | ||
|
|
223d127364 | ||
|
|
3a2c4543c4 | ||
|
|
c307f057b9 | ||
|
|
243d5ba236 | ||
|
|
3d62efccfc | ||
|
|
9d26e39025 | ||
|
|
874f0447ba | ||
|
|
0f2127a672 | ||
|
|
20d84e7dd1 | ||
|
|
a02e66023c | ||
|
|
e7db9cc772 | ||
|
|
e91fb45374 | ||
|
|
531565d55c | ||
|
|
c6307619f9 | ||
|
|
da2668cbbc | ||
|
|
765e3890b4 | ||
|
|
b40a625283 | ||
|
|
8a293985ca | ||
|
|
13b2a76c3d | ||
|
|
28e01c336d | ||
|
|
5fb0b91178 | ||
|
|
b007afc901 | ||
|
|
7f02746f63 | ||
|
|
f2788aeb36 | ||
|
|
536a05b0eb | ||
|
|
e0226d47e3 | ||
|
|
0fe10a44ce | ||
|
|
e892d81815 | ||
|
|
c58ce8450f | ||
|
|
03573d1f05 | ||
|
|
486596f89f | ||
|
|
425b4d4295 | ||
|
|
87de808046 | ||
|
|
05455ac743 | ||
|
|
1e926714cb | ||
|
|
e1602f3140 | ||
|
|
2e89973263 | ||
|
|
a6f1361ddd | ||
|
|
552a3bbd41 | ||
|
|
3f2de1e5bf | ||
|
|
059a10f475 | ||
|
|
a48abacf1d | ||
|
|
ea7ed7c030 | ||
|
|
cca899bd8c | ||
|
|
359e6b4ce2 | ||
|
|
6650dd2046 | ||
|
|
97b4a7be0f | ||
|
|
329916e200 | ||
|
|
3571b8909b | ||
|
|
c6c01b82f5 | ||
|
|
c4b575f091 | ||
|
|
fdb55568ec |
26
.github/workflows/main.yaml
vendored
26
.github/workflows/main.yaml
vendored
@@ -27,6 +27,7 @@ jobs:
|
||||
set -euo pipefail
|
||||
pip install mypy
|
||||
pip install -r requirements.txt
|
||||
pip install -r build-requirements.txt
|
||||
- name: Typecheck
|
||||
run: |
|
||||
python -m mypy --config mypy.ini -p snikket_web
|
||||
@@ -44,11 +45,34 @@ jobs:
|
||||
- name: Install
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pip install flake8
|
||||
pip install flake8 flake8-print
|
||||
- name: Linting
|
||||
run: |
|
||||
python -m flake8 snikket_web
|
||||
|
||||
translation-check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
name: 'lint: i18n'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- name: Install
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pip install flask-babel
|
||||
- name: Linting
|
||||
run: |
|
||||
sed -ri '/^"POT-Creation-Date: /d' snikket_web/translations/messages.pot
|
||||
git add snikket_web/translations/messages.pot
|
||||
make extract_translations
|
||||
sed -ri '/^"POT-Creation-Date: /d' snikket_web/translations/messages.pot
|
||||
git diff --exit-code --color -- snikket_web/translations/messages.pot
|
||||
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
54
Dockerfile
54
Dockerfile
@@ -1,7 +1,12 @@
|
||||
FROM debian:buster-slim
|
||||
FROM debian:bullseye-slim AS build
|
||||
|
||||
ARG BUILD_SERIES=dev
|
||||
ARG BUILD_ID=0
|
||||
RUN set -eu; \
|
||||
export DEBIAN_FRONTEND=noninteractive ; \
|
||||
apt-get update ; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
python3 python3-pip python3-setuptools python3-wheel \
|
||||
libpython3-dev \
|
||||
make build-essential;
|
||||
|
||||
COPY requirements.txt /opt/snikket-web-portal/requirements.txt
|
||||
COPY build-requirements.txt /opt/snikket-web-portal/build-requirements.txt
|
||||
@@ -12,30 +17,43 @@ COPY babel.cfg /opt/snikket-web-portal/babel.cfg
|
||||
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 \
|
||||
libpython3-dev \
|
||||
make build-essential \
|
||||
netcat \
|
||||
; \
|
||||
pip3 install -r requirements.txt; \
|
||||
pip3 install -r build-requirements.txt; \
|
||||
make; \
|
||||
pip3 uninstall -yr build-requirements.txt; \
|
||||
apt-get remove -y build-essential make libpython3-dev; \
|
||||
apt-get autoremove -y; \
|
||||
pip3 install hypercorn; \
|
||||
rm -rf /root/.cache; \
|
||||
apt-get clean ; rm -rf /var/lib/apt/lists
|
||||
make;
|
||||
|
||||
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
ARG BUILD_SERIES=dev
|
||||
ARG BUILD_ID=0
|
||||
|
||||
COPY docker/env.py /etc/snikket-web-portal/env.py
|
||||
|
||||
ENV SNIKKET_WEB_PYENV=/etc/snikket-web-portal/env.py
|
||||
|
||||
ENV SNIKKET_WEB_PROSODY_ENDPOINT=http://127.0.0.1:5280/
|
||||
|
||||
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 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;
|
||||
|
||||
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
|
||||
|
||||
RUN echo "$BUILD_SERIES $BUILD_ID" > /opt/snikket-web-portal/.app_version
|
||||
|
||||
ADD docker/entrypoint.sh /entrypoint.sh
|
||||
ENTRYPOINT ["/bin/sh", "/entrypoint.sh"]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
[python: snikket_web/**.py]
|
||||
[jinja2: snikket_web/templates/**.html]
|
||||
[jinja2: snikket_web/templates/**.j2]
|
||||
extensions=jinja2.ext.autoescape,jinja2.ext.with_
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pyscss~=1.3
|
||||
mypy
|
||||
python-dotenv~=0.15
|
||||
types-toml
|
||||
|
||||
@@ -5,4 +5,4 @@ export SNIKKET_WEB_DOMAIN="$SNIKKET_DOMAIN"
|
||||
export SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE="${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE-127.0.0.1}"
|
||||
export SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT="${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT-5765}"
|
||||
|
||||
exec hypercorn -b "${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE}:${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT}" 'snikket_web:create_app()'
|
||||
exec hypercorn -b "${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE}:${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT}" --access-logfile=- --log-file=- 'snikket_web:create_app()'
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
aiohttp~=3.6
|
||||
quart~=0.11
|
||||
flask-wtf~=0.14
|
||||
hsluv~=0.0.2
|
||||
quart~=0.17,<0.18
|
||||
flask-wtf~=1.0
|
||||
hsluv~=5.0
|
||||
flask-babel~=1.0
|
||||
email-validator~=1.1
|
||||
environ-config~=20.0
|
||||
wtforms~=3.0
|
||||
typing-extensions
|
||||
|
||||
@@ -18,10 +18,12 @@ from quart import (
|
||||
jsonify,
|
||||
)
|
||||
|
||||
import werkzeug.exceptions
|
||||
|
||||
import environ
|
||||
|
||||
from . import colour, infra
|
||||
from ._version import version, version_info # noqa:F401
|
||||
from ._version import version # noqa:F401
|
||||
|
||||
|
||||
async def proc() -> typing.Dict[str, typing.Any]:
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -145,21 +147,29 @@ class AppConfig:
|
||||
site_name = environ.var("")
|
||||
avatar_cache_ttl = environ.var(1800, converter=int)
|
||||
languages = environ.var([
|
||||
# Keep `en` as the first language, because it is used as a fallback
|
||||
# if the language negotiation cannot find another match. It is more
|
||||
# likely that users are able to read english (or find a suitable
|
||||
# online translator) than, for instance, danish.
|
||||
"en",
|
||||
"da",
|
||||
"de",
|
||||
"en",
|
||||
"fr",
|
||||
"id",
|
||||
"it",
|
||||
"pl",
|
||||
"sv",
|
||||
"zh_Hans_CN",
|
||||
], converter=autosplit)
|
||||
apple_store_url = environ.var("")
|
||||
apple_store_url = environ.var(
|
||||
"https://apps.apple.com/us/app/snikket/id1545164189",
|
||||
)
|
||||
# Default limit of 1 MiB is what was discovered to be the effective limit
|
||||
# in #67, hence we set that here for now.
|
||||
# Future versions may change this default, and the standard deployment
|
||||
# tools may also very well override it.
|
||||
max_avatar_size = environ.var(1024*1024, converter=int)
|
||||
show_metrics = environ.bool_var(True)
|
||||
|
||||
|
||||
_UPPER_CASE = "".join(map(chr, range(ord("A"), ord("Z")+1)))
|
||||
@@ -172,7 +182,7 @@ def create_app() -> quart.Quart:
|
||||
pass
|
||||
else:
|
||||
import runpy
|
||||
init_vars = runpy.run_path(env_init) # type:ignore
|
||||
init_vars = runpy.run_path(env_init)
|
||||
for name, value in init_vars.items():
|
||||
if not name:
|
||||
continue
|
||||
@@ -191,23 +201,24 @@ def create_app() -> quart.Quart:
|
||||
app.config["AVATAR_CACHE_TTL"] = config.avatar_cache_ttl
|
||||
app.config["APPLE_STORE_URL"] = config.apple_store_url
|
||||
app.config["MAX_AVATAR_SIZE"] = config.max_avatar_size
|
||||
app.config["SHOW_METRICS"] = config.show_metrics
|
||||
|
||||
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'))
|
||||
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
version_info = (0, 1, 2, "a0")
|
||||
version = (
|
||||
".".join(map(str, version_info[:3])) +
|
||||
(f"-{version_info[3]}" if version_info[3] else "")
|
||||
)
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
version = "(unknown)"
|
||||
|
||||
if os.path.exists(".app_version"):
|
||||
with open(".app_version") as f:
|
||||
version = f.read().strip()
|
||||
elif os.path.exists(".git"):
|
||||
try:
|
||||
version = subprocess.check_output([
|
||||
"git", "describe", "--always"
|
||||
]).strip().decode("utf8")
|
||||
except OSError:
|
||||
version = "dev (unknown)"
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import json
|
||||
import resource
|
||||
import time
|
||||
import typing
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import aiohttp
|
||||
|
||||
import werkzeug.exceptions
|
||||
|
||||
import quart.flask_patch
|
||||
|
||||
import wtforms
|
||||
import wtforms.fields.html5
|
||||
|
||||
from quart import (
|
||||
Blueprint,
|
||||
@@ -18,11 +21,12 @@ from quart import (
|
||||
request,
|
||||
abort,
|
||||
flash,
|
||||
current_app,
|
||||
)
|
||||
|
||||
from flask_babel import lazy_gettext as _l, _
|
||||
|
||||
from . import prosodyclient
|
||||
from . import prosodyclient, _version
|
||||
from .infra import client, circle_name, BaseForm
|
||||
|
||||
bp = Blueprint("admin", __name__, url_prefix="/admin")
|
||||
@@ -31,11 +35,14 @@ bp = Blueprint("admin", __name__, url_prefix="/admin")
|
||||
@bp.route("/")
|
||||
@client.require_admin_session()
|
||||
async def index() -> str:
|
||||
return await render_template("admin_home.html")
|
||||
show_metrics = current_app.config["SHOW_METRICS"]
|
||||
return await render_template(
|
||||
"admin_home.html",
|
||||
show_metrics=show_metrics,
|
||||
)
|
||||
|
||||
|
||||
class PasswordResetLinkPost(BaseForm):
|
||||
action_create = wtforms.StringField()
|
||||
action_revoke = wtforms.StringField()
|
||||
|
||||
|
||||
@@ -57,6 +64,82 @@ async def users() -> str:
|
||||
)
|
||||
|
||||
|
||||
class EditUserForm(BaseForm):
|
||||
localpart = wtforms.StringField(
|
||||
_l("Login name"),
|
||||
)
|
||||
|
||||
display_name = wtforms.StringField(
|
||||
_l("Display name"),
|
||||
)
|
||||
|
||||
role = wtforms.RadioField(
|
||||
_l("Access Level"),
|
||||
choices=[
|
||||
("prosody:restricted", _("Limited")),
|
||||
("prosody:normal", _l("Normal user")),
|
||||
("prosody:admin", _l("Administrator")),
|
||||
],
|
||||
)
|
||||
|
||||
action_save = wtforms.SubmitField(
|
||||
_l("Update user"),
|
||||
)
|
||||
|
||||
action_create_reset = wtforms.SubmitField(
|
||||
_l("Create password reset link"),
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/user/<localpart>/", methods=["GET", "POST"])
|
||||
@client.require_admin_session()
|
||||
async def edit_user(localpart: str) -> typing.Union[werkzeug.Response, str]:
|
||||
target_user_info = await client.get_user_by_localpart(localpart)
|
||||
|
||||
form = EditUserForm()
|
||||
if form.validate_on_submit():
|
||||
if form.action_create_reset.data:
|
||||
target_user_info = await client.get_user_by_localpart(localpart)
|
||||
reset_link = await client.create_password_reset_invite(
|
||||
localpart=localpart,
|
||||
ttl=86400,
|
||||
)
|
||||
await flash(
|
||||
_("Password reset link created"),
|
||||
"success",
|
||||
)
|
||||
return redirect(url_for(
|
||||
".user_password_reset_link",
|
||||
id_=reset_link.id_,
|
||||
))
|
||||
|
||||
await client.update_user(
|
||||
localpart,
|
||||
display_name=form.display_name.data,
|
||||
roles=[form.role.data],
|
||||
)
|
||||
|
||||
await flash(
|
||||
_("User information updated."),
|
||||
"success",
|
||||
)
|
||||
return redirect(url_for(".edit_user", localpart=localpart))
|
||||
|
||||
elif request.method == "GET":
|
||||
form.localpart.data = target_user_info.localpart
|
||||
form.display_name.data = target_user_info.display_name
|
||||
if target_user_info.roles:
|
||||
form.role.data = target_user_info.roles[0]
|
||||
else:
|
||||
form.role.data = "prosody:normal"
|
||||
|
||||
return await render_template(
|
||||
"admin_edit_user.html",
|
||||
target_user=target_user_info,
|
||||
form=form,
|
||||
)
|
||||
|
||||
|
||||
class DeleteUserForm(BaseForm):
|
||||
action_delete = wtforms.SubmitField(
|
||||
_l("Delete user permanently")
|
||||
@@ -65,7 +148,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():
|
||||
@@ -100,36 +183,38 @@ async def debug_user(localpart: str) -> typing.Union[str, quart.Response]:
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/users/password-reset/-", methods=["POST"])
|
||||
@bp.route("/users/password-reset/<id_>", methods=["GET", "POST"])
|
||||
@client.require_admin_session()
|
||||
async def create_password_reset_link() -> typing.Union[str, quart.Response]:
|
||||
form = PasswordResetLinkPost()
|
||||
if not form.validate_on_submit():
|
||||
abort(400)
|
||||
|
||||
if form.action_create.data:
|
||||
localpart = form.action_create.data
|
||||
target_user_info = await client.get_user_by_localpart(localpart)
|
||||
reset_link = await client.create_password_reset_invite(
|
||||
localpart=localpart,
|
||||
ttl=86400,
|
||||
)
|
||||
async def user_password_reset_link(
|
||||
id_: str,
|
||||
) -> typing.Union[str, werkzeug.Response]:
|
||||
invite_info = await client.get_invite_by_id(
|
||||
id_,
|
||||
)
|
||||
if invite_info.jid is None:
|
||||
await flash(
|
||||
_("Password reset link created"),
|
||||
"success",
|
||||
)
|
||||
elif form.action_revoke.data:
|
||||
await client.delete_invite(form.action_revoke.data)
|
||||
await flash(
|
||||
_("Password reset link deleted"),
|
||||
"success",
|
||||
_("Password reset link not found"),
|
||||
"alert",
|
||||
)
|
||||
return redirect(url_for(".users"))
|
||||
|
||||
localpart = prosodyclient.split_jid(invite_info.jid)[0]
|
||||
|
||||
form = PasswordResetLinkPost()
|
||||
if form.validate_on_submit():
|
||||
if form.action_revoke.data:
|
||||
await client.delete_invite(id_)
|
||||
await flash(
|
||||
_("Password reset link deleted"),
|
||||
"success",
|
||||
)
|
||||
return redirect(url_for(".edit_user", localpart=localpart))
|
||||
abort(400)
|
||||
|
||||
return await render_template(
|
||||
"admin_reset_user_password.html",
|
||||
target_user=target_user_info,
|
||||
reset_link=reset_link,
|
||||
localpart=localpart,
|
||||
reset_link=invite_info,
|
||||
form=form,
|
||||
)
|
||||
|
||||
@@ -194,7 +279,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
|
||||
@@ -240,7 +325,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 = [
|
||||
@@ -268,7 +353,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:
|
||||
@@ -334,7 +419,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(
|
||||
@@ -380,7 +465,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(
|
||||
@@ -396,21 +481,21 @@ async def edit_circle(id_: str) -> typing.Union[str, quart.Response]:
|
||||
return redirect(url_for(".circles"))
|
||||
raise
|
||||
|
||||
users = sorted(
|
||||
await client.list_users(),
|
||||
key=lambda x: x.localpart
|
||||
)
|
||||
users = {
|
||||
user.localpart: user
|
||||
for user in await client.list_users()
|
||||
}
|
||||
circle_members = [
|
||||
user for user in users
|
||||
if user.localpart in circle.members
|
||||
(localpart, users.get(localpart))
|
||||
for localpart in sorted(circle.members)
|
||||
]
|
||||
|
||||
form = EditCircleForm()
|
||||
form.user_to_add.choices = [
|
||||
(user.localpart, user.localpart)
|
||||
for user in users
|
||||
if user.localpart not in circle.members
|
||||
]
|
||||
form.user_to_add.choices = sorted(
|
||||
(localpart, localpart)
|
||||
for localpart in users.keys()
|
||||
if localpart not in circle.members
|
||||
)
|
||||
valid_users = [x[0] for x in form.user_to_add.choices]
|
||||
|
||||
invite_form = InvitePost()
|
||||
@@ -458,8 +543,6 @@ async def edit_circle(id_: str) -> typing.Union[str, quart.Response]:
|
||||
)
|
||||
|
||||
return redirect(url_for(".edit_circle", id_=id_))
|
||||
else:
|
||||
print(form.errors)
|
||||
|
||||
return await render_template(
|
||||
"admin_edit_circle.html",
|
||||
@@ -468,3 +551,153 @@ async def edit_circle(id_: str) -> typing.Union[str, quart.Response]:
|
||||
circle_members=circle_members,
|
||||
invite_form=invite_form,
|
||||
)
|
||||
|
||||
|
||||
_CPU_EPOCH = time.process_time()
|
||||
_MONOTONIC_EPOCH = time.monotonic()
|
||||
|
||||
|
||||
def get_system_stats() -> typing.MutableMapping[
|
||||
str,
|
||||
typing.Optional[typing.Union[int, float]]]:
|
||||
pagesize = resource.getpagesize()
|
||||
my_rss: typing.Optional[int] = None
|
||||
try:
|
||||
with open("/proc/self/statm") as f:
|
||||
stats = f.read().split()
|
||||
my_rss = int(stats[1]) * pagesize
|
||||
except (ValueError, IndexError, TypeError, OSError):
|
||||
pass
|
||||
|
||||
my_cpu = (
|
||||
(time.process_time() - _CPU_EPOCH) /
|
||||
(time.monotonic() - _MONOTONIC_EPOCH)
|
||||
)
|
||||
|
||||
mem_total, mem_available = None, None
|
||||
load5: typing.Optional[float] = None
|
||||
|
||||
try:
|
||||
with open("/proc/loadavg") as f:
|
||||
stats = f.read().split()
|
||||
load5 = float(stats[1])
|
||||
except (ValueError, IndexError, TypeError, OSError):
|
||||
pass
|
||||
|
||||
try:
|
||||
with open("/proc/meminfo") as f:
|
||||
for line in f:
|
||||
if line.startswith("MemTotal"):
|
||||
mem_total = int(line.split()[1]) * 1024
|
||||
elif line.startswith("MemAvailable"):
|
||||
mem_available = int(line.split()[1]) * 1024
|
||||
if mem_total is not None and mem_available is not None:
|
||||
break
|
||||
except (ValueError, TypeError, IndexError, OSError):
|
||||
pass
|
||||
|
||||
return {
|
||||
"portal_rss": my_rss,
|
||||
"portal_cpu": my_cpu,
|
||||
"load5": load5,
|
||||
"mem_total": mem_total,
|
||||
"mem_available": mem_available,
|
||||
}
|
||||
|
||||
|
||||
class AnnouncementForm(BaseForm):
|
||||
text = wtforms.StringField(
|
||||
_("Message contents"),
|
||||
widget=wtforms.widgets.TextArea(),
|
||||
validators=[wtforms.validators.DataRequired()],
|
||||
)
|
||||
|
||||
online_only = wtforms.BooleanField(
|
||||
_("Only send to online users"),
|
||||
)
|
||||
|
||||
action_post_all = wtforms.SubmitField(
|
||||
_("Post to all users"),
|
||||
)
|
||||
|
||||
action_send_preview = wtforms.SubmitField(
|
||||
_("Send preview to yourself"),
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/system/", methods=["GET", "POST"])
|
||||
@client.require_admin_session()
|
||||
async def system() -> typing.Union[str, werkzeug.Response]:
|
||||
form = AnnouncementForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
recipients = "self"
|
||||
if form.action_post_all.data:
|
||||
if form.online_only.data:
|
||||
recipients = "online"
|
||||
else:
|
||||
recipients = "all"
|
||||
|
||||
await client.post_announcement(
|
||||
form.text.data,
|
||||
recipients=recipients,
|
||||
)
|
||||
await flash(
|
||||
_("Announcement sent!"),
|
||||
"success",
|
||||
)
|
||||
if recipients != "self":
|
||||
# redirect only if not previewing
|
||||
return redirect(url_for(".system"))
|
||||
|
||||
version = None
|
||||
now = None
|
||||
show_metrics = current_app.config["SHOW_METRICS"]
|
||||
if show_metrics:
|
||||
version = await client.get_server_version()
|
||||
now = time.time()
|
||||
try:
|
||||
prosody_metrics = await client.get_system_metrics()
|
||||
except werkzeug.exceptions.NotFound:
|
||||
# server does not offer the endpoint for whatever reason -- ignore
|
||||
prosody_metrics = {}
|
||||
|
||||
metrics = get_system_stats()
|
||||
try:
|
||||
prosody_cpu_metrics = prosody_metrics["cpu"]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
metrics["prosody_cpu"] = (prosody_cpu_metrics["value"] /
|
||||
(now - prosody_cpu_metrics["since"]))
|
||||
|
||||
try:
|
||||
metrics["prosody_rss"] = prosody_metrics["memory"]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
metrics["prosody_devices"] = prosody_metrics["c2s"]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
metrics["prosody_uploads"] = prosody_metrics["uploads"]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
for k in list(metrics.keys()):
|
||||
if metrics[k] is None:
|
||||
# so that defaulting in jinja works
|
||||
del metrics[k]
|
||||
else:
|
||||
metrics = {}
|
||||
|
||||
return await render_template(
|
||||
"admin_system.html",
|
||||
metrics=metrics,
|
||||
version=_version.version,
|
||||
prosody_version=version,
|
||||
form=form,
|
||||
show_metrics=show_metrics,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import base64
|
||||
import itertools
|
||||
import math
|
||||
import secrets
|
||||
import typing
|
||||
|
||||
@@ -7,6 +8,7 @@ import quart.flask_patch # noqa:F401
|
||||
from quart import (
|
||||
current_app,
|
||||
request,
|
||||
g,
|
||||
)
|
||||
|
||||
import flask_babel
|
||||
@@ -22,11 +24,21 @@ client.default_login_redirect = "main.login"
|
||||
babel = flask_babel.Babel()
|
||||
|
||||
|
||||
BYTE_UNIT_SCALE_MAP = [
|
||||
"B",
|
||||
"kiB",
|
||||
"MiB",
|
||||
"GiB",
|
||||
"TiB",
|
||||
]
|
||||
|
||||
|
||||
@babel.localeselector # type:ignore
|
||||
def selected_locale() -> str:
|
||||
g.language_header_accessed = True
|
||||
selected = request.accept_languages.best_match(
|
||||
current_app.config['LANGUAGES']
|
||||
)
|
||||
) or current_app.config['LANGUAGES'][0]
|
||||
return selected
|
||||
|
||||
|
||||
@@ -42,14 +54,39 @@ def circle_name(c: typing.Any) -> str:
|
||||
return c.name
|
||||
|
||||
|
||||
def format_bytes(n: float) -> str:
|
||||
try:
|
||||
scale = max(math.floor(math.log(n, 1024)), 0)
|
||||
except ValueError:
|
||||
scale = 0
|
||||
try:
|
||||
unit = BYTE_UNIT_SCALE_MAP[scale]
|
||||
factor = 1024**scale
|
||||
except IndexError:
|
||||
unit = "TiB"
|
||||
factor = 1024**4
|
||||
if factor > 1:
|
||||
return "{:.1f} {}".format(n / factor, unit)
|
||||
return "{} {}".format(n, unit)
|
||||
|
||||
|
||||
def add_vary_language_header(resp: quart.Response) -> quart.Response:
|
||||
if getattr(g, "language_header_accessed", False):
|
||||
resp.vary.add("Accept-Language")
|
||||
return resp
|
||||
|
||||
|
||||
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("format_percent")(flask_babel.format_percent)
|
||||
app.template_filter("format_bytes")(format_bytes)
|
||||
app.template_filter("flatten")(flatten)
|
||||
app.template_filter("circle_name")(circle_name)
|
||||
app.after_request(add_vary_language_header)
|
||||
|
||||
|
||||
def generate_error_id() -> str:
|
||||
|
||||
@@ -10,13 +10,16 @@ from quart import (
|
||||
current_app,
|
||||
render_template,
|
||||
redirect,
|
||||
request,
|
||||
url_for,
|
||||
session as http_session,
|
||||
)
|
||||
|
||||
import werkzeug
|
||||
|
||||
import wtforms
|
||||
|
||||
from flask_babel import lazy_gettext as _l
|
||||
from flask_babel import lazy_gettext as _l, gettext
|
||||
|
||||
from .infra import client, selected_locale, BaseForm
|
||||
|
||||
@@ -26,6 +29,11 @@ bp = Blueprint("invite", __name__)
|
||||
|
||||
INVITE_SESSION_JID = "invite-session-jid"
|
||||
|
||||
MAX_IMPORT_DATA_SIZE = 5*1024*1024 # 5MB
|
||||
SUPPORTED_IMPORT_TYPES = ["application/xml", "text/xml"]
|
||||
|
||||
EIMPORTTOOBIG = _l("The account data you tried to import is too large to"
|
||||
" upload. Please contact your Snikket operator.")
|
||||
|
||||
# https://play.google.com/store/apps/details?id=org.snikket.android&referrer={uri|urlescape}&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1
|
||||
|
||||
@@ -40,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_))
|
||||
|
||||
|
||||
@@ -96,7 +104,7 @@ async def view(id_: str) -> typing.Union[quart.Response,
|
||||
return quart.Response(
|
||||
body,
|
||||
headers={
|
||||
"Link": "<{}> rel=\"alternate\"".format(invite.xmpp_uri),
|
||||
"Link": "<{}>; rel=\"alternate\"".format(invite.xmpp_uri),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -125,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:
|
||||
@@ -163,6 +171,7 @@ async def register(id_: str) -> typing.Union[str, quart.Response]:
|
||||
raise
|
||||
else:
|
||||
http_session[INVITE_SESSION_JID] = jid
|
||||
await client.login(jid, form.password.data)
|
||||
return redirect(url_for(".success"))
|
||||
|
||||
return await render_template(
|
||||
@@ -192,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:
|
||||
@@ -232,11 +241,55 @@ async def reset(id_: str) -> typing.Union[str, quart.Response]:
|
||||
)
|
||||
|
||||
|
||||
class DataImportForm(BaseForm):
|
||||
account_data_file = wtforms.FileField(
|
||||
_l("Account data file")
|
||||
)
|
||||
|
||||
action_import = wtforms.SubmitField(
|
||||
_l("Import data")
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/success", methods=["GET", "POST"])
|
||||
@client.require_session()
|
||||
async def success() -> str:
|
||||
form = DataImportForm()
|
||||
if form.validate_on_submit():
|
||||
ok = True
|
||||
file_info = (await request.files).get(form.account_data_file.name)
|
||||
if file_info is not None:
|
||||
mimetype = file_info.mimetype
|
||||
data = file_info.stream.read()
|
||||
if len(data) > MAX_IMPORT_DATA_SIZE:
|
||||
form.account_data_file.errors.append(EIMPORTTOOBIG)
|
||||
ok = False
|
||||
elif mimetype not in SUPPORTED_IMPORT_TYPES:
|
||||
form.account_data_file.errors.append(
|
||||
# not breaking the line here to avoid extract
|
||||
# translations failing (defensive)
|
||||
gettext("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).", mimetype=mimetype), # noqa:E501
|
||||
)
|
||||
ok = False
|
||||
elif len(data) > 0:
|
||||
await client.import_account_data(data)
|
||||
|
||||
if ok:
|
||||
# Re-render success page, this time with no import option
|
||||
return await render_template(
|
||||
"invite_success.html",
|
||||
jid=http_session.get(INVITE_SESSION_JID, ""),
|
||||
migration_success=True,
|
||||
)
|
||||
|
||||
return await render_template(
|
||||
"invite_success.html",
|
||||
jid=http_session.get(INVITE_SESSION_JID, ""),
|
||||
migration_success=False,
|
||||
form=form,
|
||||
max_import_size=MAX_IMPORT_DATA_SIZE,
|
||||
import_too_big_warning_header=_l("Error"),
|
||||
import_too_big_warning=EIMPORTTOOBIG,
|
||||
)
|
||||
|
||||
|
||||
@@ -249,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"))
|
||||
|
||||
@@ -18,6 +18,8 @@ from quart import (
|
||||
flash,
|
||||
)
|
||||
|
||||
import werkzeug.exceptions
|
||||
|
||||
import babel
|
||||
import wtforms
|
||||
|
||||
@@ -32,7 +34,7 @@ bp = quart.Blueprint("main", __name__)
|
||||
|
||||
|
||||
class LoginForm(BaseForm):
|
||||
address = wtforms.TextField(
|
||||
address = wtforms.StringField(
|
||||
_l("Address"),
|
||||
validators=[wtforms.validators.InputRequired()],
|
||||
)
|
||||
@@ -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(
|
||||
@@ -91,24 +93,30 @@ async def login() -> typing.Union[str, quart.Response]:
|
||||
@bp.route("/meta/about.html")
|
||||
async def about() -> str:
|
||||
version = None
|
||||
core_versions = {}
|
||||
extra_versions = {}
|
||||
|
||||
if current_app.debug or client.is_admin_session:
|
||||
version = _version.version
|
||||
extra_versions["Quart"] = quart.__version__
|
||||
try:
|
||||
core_versions["Prosody"] = await client.get_server_version()
|
||||
except werkzeug.exceptions.Unauthorized:
|
||||
core_versions["Prosody"] = "unknown"
|
||||
|
||||
if current_app.debug:
|
||||
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(
|
||||
"about.html",
|
||||
version=version,
|
||||
extra_versions=extra_versions,
|
||||
core_versions=core_versions,
|
||||
)
|
||||
|
||||
|
||||
@@ -123,6 +131,7 @@ def repad(s: str) -> str:
|
||||
|
||||
@bp.route("/avatar/<from_>/<code>")
|
||||
async def avatar(from_: str, code: str) -> quart.Response:
|
||||
etag: typing.Optional[str]
|
||||
try:
|
||||
etag = request.headers["if-none-match"]
|
||||
except KeyError:
|
||||
|
||||
@@ -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
|
||||
@@ -44,6 +46,15 @@ class AdminUserInfo:
|
||||
display_name: typing.Optional[str]
|
||||
email: typing.Optional[str]
|
||||
phone: typing.Optional[str]
|
||||
roles: typing.Optional[typing.List[str]]
|
||||
|
||||
@property
|
||||
def has_admin_role(self) -> bool:
|
||||
return bool(self.roles and "prosody:admin" in self.roles)
|
||||
|
||||
@property
|
||||
def has_restricted_role(self) -> bool:
|
||||
return bool(self.roles and "prosody:restricted" in self.roles)
|
||||
|
||||
@classmethod
|
||||
def from_api_response(
|
||||
@@ -55,6 +66,7 @@ class AdminUserInfo:
|
||||
display_name=data.get("display_name") or None,
|
||||
email=data.get("email") or None,
|
||||
phone=data.get("phone") or None,
|
||||
roles=data.get("roles"),
|
||||
)
|
||||
|
||||
|
||||
@@ -286,6 +298,9 @@ class ProsodyClient:
|
||||
def _public_v1_endpoint(self, subpath: str) -> str:
|
||||
return "{}/register_api{}".format(self._endpoint_base, subpath)
|
||||
|
||||
def _xep227_endpoint(self, subpath: str) -> str:
|
||||
return "{}/xep227{}".format(self._endpoint_base, subpath)
|
||||
|
||||
async def _oauth2_bearer_token(self,
|
||||
session: aiohttp.ClientSession,
|
||||
jid: str,
|
||||
@@ -373,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:
|
||||
@@ -402,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.")
|
||||
|
||||
@@ -479,7 +494,7 @@ class ProsodyClient:
|
||||
session=session,
|
||||
)
|
||||
avatar_hash = avatar_info["sha1"]
|
||||
except quart.exceptions.HTTPException:
|
||||
except werkzeug.exceptions.HTTPException:
|
||||
avatar_hash = None
|
||||
|
||||
return {
|
||||
@@ -500,7 +515,7 @@ class ProsodyClient:
|
||||
"to": self.session_address,
|
||||
}
|
||||
|
||||
async with session.post(self._rest_endpoint, data=req) as resp:
|
||||
async with session.post(self._rest_endpoint, json=req) as resp:
|
||||
return resp.status == 200
|
||||
|
||||
@autosession
|
||||
@@ -509,11 +524,11 @@ class ProsodyClient:
|
||||
req = {
|
||||
"kind": "iq",
|
||||
"type": "get",
|
||||
"version": True,
|
||||
"version": {},
|
||||
"to": domain,
|
||||
}
|
||||
|
||||
async with session.post(self._rest_endpoint, data=req) as resp:
|
||||
async with session.post(self._rest_endpoint, json=req) as resp:
|
||||
if resp.status != 200:
|
||||
return "unknwn"
|
||||
try:
|
||||
@@ -631,7 +646,7 @@ class ProsodyClient:
|
||||
new_access_model,
|
||||
)
|
||||
))
|
||||
except quart.exceptions.NotFound:
|
||||
except werkzeug.exceptions.NotFound:
|
||||
if ignore_not_found:
|
||||
return
|
||||
raise
|
||||
@@ -761,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),
|
||||
@@ -858,6 +873,29 @@ class ProsodyClient:
|
||||
self._raise_error_from_response(resp)
|
||||
return AdminUserInfo.from_api_response(await resp.json())
|
||||
|
||||
@autosession
|
||||
async def update_user(
|
||||
self,
|
||||
localpart: str,
|
||||
*,
|
||||
display_name: typing.Optional[str],
|
||||
roles: typing.Optional[typing.Collection[str]],
|
||||
session: aiohttp.ClientSession,
|
||||
) -> None:
|
||||
payload: typing.Dict[str, typing.Any] = {
|
||||
"username": localpart,
|
||||
}
|
||||
if display_name is not None:
|
||||
payload["display_name"] = display_name
|
||||
if roles is not None:
|
||||
payload["roles"] = list(roles)
|
||||
|
||||
async with session.put(
|
||||
self._admin_v1_endpoint("/users/{}".format(localpart)),
|
||||
json=payload,
|
||||
) as resp:
|
||||
self._raise_error_from_response(resp)
|
||||
|
||||
@autosession
|
||||
async def get_user_debug_info(
|
||||
self,
|
||||
@@ -1088,6 +1126,34 @@ class ProsodyClient:
|
||||
) as resp:
|
||||
self._raise_error_from_response(resp)
|
||||
|
||||
@autosession
|
||||
async def export_account_data(
|
||||
self,
|
||||
*,
|
||||
session: aiohttp.ClientSession,
|
||||
) -> typing.Optional[str]:
|
||||
async with session.get(
|
||||
self._xep227_endpoint("/export?stores=roster,vcard,pep,pep_data"), # noqa:E501
|
||||
) as resp:
|
||||
self._raise_error_from_response(resp)
|
||||
if resp.status == 204:
|
||||
return None
|
||||
return await resp.text()
|
||||
|
||||
@autosession
|
||||
async def import_account_data(
|
||||
self,
|
||||
user_xml: str,
|
||||
*,
|
||||
session: aiohttp.ClientSession,
|
||||
) -> bool:
|
||||
async with session.put(
|
||||
self._xep227_endpoint("/import?stores=roster,vcard,pep,pep_data"), # noqa:E501
|
||||
data=user_xml,
|
||||
) as resp:
|
||||
self._raise_error_from_response(resp)
|
||||
return True
|
||||
|
||||
@autosession
|
||||
async def revoke_token(
|
||||
self,
|
||||
@@ -1142,3 +1208,41 @@ class ProsodyClient:
|
||||
json=payload) as resp:
|
||||
resp.raise_for_status()
|
||||
return (await resp.json())["jid"]
|
||||
|
||||
@autosession
|
||||
async def get_system_metrics(
|
||||
self,
|
||||
*,
|
||||
session: aiohttp.ClientSession) -> typing.Mapping:
|
||||
async with session.get(
|
||||
self._admin_v1_endpoint("/server/metrics"),
|
||||
) as resp:
|
||||
if resp.status == 404:
|
||||
return {}
|
||||
self._raise_error_from_response(resp)
|
||||
resp.raise_for_status()
|
||||
return await resp.json()
|
||||
|
||||
@autosession
|
||||
async def post_announcement(
|
||||
self,
|
||||
body: str,
|
||||
recipients: str,
|
||||
*,
|
||||
session: aiohttp.ClientSession) -> None:
|
||||
recipients_payload: typing.Union[str, typing.Sequence[str]]
|
||||
if recipients == "self":
|
||||
recipients_payload = [self.session_address]
|
||||
else:
|
||||
recipients_payload = recipients
|
||||
|
||||
payload = {
|
||||
"recipients": recipients_payload,
|
||||
"body": body,
|
||||
}
|
||||
|
||||
async with session.post(
|
||||
self._admin_v1_endpoint("/server/announcement"),
|
||||
json=payload) as resp:
|
||||
self._raise_error_from_response(resp)
|
||||
resp.raise_for_status()
|
||||
|
||||
@@ -354,6 +354,15 @@ div.form.layout-expanded {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.radio-button-ext > label > p {
|
||||
margin-left: 1.75rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.radio-button-ext > label .icon {
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
|
||||
div.select-wrap {
|
||||
display: block;
|
||||
border-bottom: $w-s4 solid $primary-500;
|
||||
|
||||
@@ -80,60 +80,6 @@ img.fdroid {
|
||||
height: $w-l3;
|
||||
}
|
||||
|
||||
.tabbox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: $w-l1 0;
|
||||
|
||||
> nav.tabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
> a {
|
||||
display: inline-block;
|
||||
padding: $w-s2;
|
||||
border-top-left-radius: $w-s4;
|
||||
border-top-right-radius: $w-s4;
|
||||
|
||||
&, &:visited {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: $accent-500;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $accent-900;
|
||||
border-color: $accent-800;
|
||||
color: black;
|
||||
}
|
||||
|
||||
&.active {
|
||||
text-decoration: none;
|
||||
background: linear-gradient(0deg, $accent-600, $accent-700);
|
||||
color: $accent-200;
|
||||
|
||||
&:hover, &:focus {
|
||||
background: linear-gradient(0deg, $accent-700, $accent-800);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: $accent-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .tab-pane {
|
||||
display: none;
|
||||
padding: 0 $w-0;
|
||||
background: $accent-900;
|
||||
|
||||
&.active {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qr {
|
||||
margin: $w-l1 0;
|
||||
display: flex;
|
||||
|
||||
@@ -37,6 +37,16 @@ licensed under the terms of the Apache 2.0 License -->
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M10.79 16.29c.39.39 1.02.39 1.41 0l3.59-3.59c.39-.39.39-1.02 0-1.41L12.2 7.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41L12.67 11H4c-.55 0-1 .45-1 1s.45 1 1 1h8.67l-1.88 1.88c-.39.39-.38 1.03 0 1.41zM19 3H5c-1.11 0-2 .9-2 2v3c0 .55.45 1 1 1s1-.45 1-1V6c0-.55.45-1 1-1h12c.55 0 1 .45 1 1v12c0 .55-.45 1-1 1H6c-.55 0-1-.45-1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1v3c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z" />
|
||||
</symbol>
|
||||
<!-- from: action/lock/materialiconsround/24px.svg -->
|
||||
<symbol id="icon-lock" viewBox="0 0 24 24">
|
||||
<g fill="none"><path d="M0 0h24v24H0V0z" /><path d="M0 0h24v24H0V0z" opacity=".87" /></g>
|
||||
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zM9 8V6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9z" />
|
||||
</symbol>
|
||||
<!-- from: communication/import_export/materialiconsround/24px.svg -->
|
||||
<symbol id="icon-import_export" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M8.65 3.35L5.86 6.14c-.32.31-.1.85.35.85H8V13c0 .55.45 1 1 1s1-.45 1-1V6.99h1.79c.45 0 .67-.54.35-.85L9.35 3.35c-.19-.19-.51-.19-.7 0zM16 17.01V11c0-.55-.45-1-1-1s-1 .45-1 1v6.01h-1.79c-.45 0-.67.54-.35.85l2.79 2.78c.2.19.51.19.71 0l2.79-2.78c.32-.31.09-.85-.35-.85H16z" />
|
||||
</symbol>
|
||||
<!-- from: communication/qr_code/materialiconsround/24px.svg -->
|
||||
<symbol id="icon-qrcode" viewBox="0 0 24 24">
|
||||
<g><rect fill="none" height="24" width="24" /><rect fill="none" height="24" width="24" /></g>
|
||||
@@ -47,6 +57,12 @@ licensed under the terms of the Apache 2.0 License -->
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M12.65 10C11.7 7.31 8.9 5.5 5.77 6.12c-2.29.46-4.15 2.29-4.63 4.58C.32 14.57 3.26 18 7 18c2.61 0 4.83-1.67 5.65-4H17v2c0 1.1.9 2 2 2s2-.9 2-2v-2c1.1 0 2-.9 2-2s-.9-2-2-2h-8.35zM7 14c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z" />
|
||||
</symbol>
|
||||
<!-- from: communication/rss_feed/materialiconsround/24px.svg -->
|
||||
<symbol id="icon-broadcast" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<circle cx="6.18" cy="17.82" r="2.18" />
|
||||
<path d="M5.59 10.23c-.84-.14-1.59.55-1.59 1.4 0 .71.53 1.28 1.23 1.4 2.92.51 5.22 2.82 5.74 5.74.12.7.69 1.23 1.4 1.23.85 0 1.54-.75 1.41-1.59-.68-4.2-3.99-7.51-8.19-8.18zm-.03-5.71C4.73 4.43 4 5.1 4 5.93c0 .73.55 1.33 1.27 1.4 6.01.6 10.79 5.38 11.39 11.39.07.73.67 1.28 1.4 1.28.84 0 1.5-.73 1.42-1.56-.73-7.34-6.57-13.19-13.92-13.92z" />
|
||||
</symbol>
|
||||
<!-- from: content/add_circle_outline/materialiconsround/24px.svg -->
|
||||
<symbol id="icon-add" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
@@ -72,6 +88,26 @@ licensed under the terms of the Apache 2.0 License -->
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M21.94 11.23C21.57 8.76 19.32 7 16.82 7h-2.87c-.52 0-.95.43-.95.95s.43.95.95.95h2.9c1.6 0 3.04 1.14 3.22 2.73.17 1.43-.64 2.69-1.85 3.22l1.4 1.4c1.63-1.02 2.64-2.91 2.32-5.02zM4.12 3.56c-.39-.39-1.02-.39-1.41 0s-.39 1.02 0 1.41l2.4 2.4c-1.94.8-3.27 2.77-3.09 5.04C2.23 15.05 4.59 17 7.23 17h2.82c.52 0 .95-.43.95-.95s-.43-.95-.95-.95H7.16c-1.63 0-3.1-1.19-3.25-2.82-.15-1.72 1.11-3.17 2.75-3.35l2.1 2.1c-.43.09-.76.46-.76.92v.1c0 .52.43.95.95.95h1.78L13 15.27V17h1.73l3.3 3.3c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41L4.12 3.56zM16 11.95c0-.52-.43-.95-.95-.95h-.66l1.49 1.49c.07-.13.12-.28.12-.44v-.1z" />
|
||||
</symbol>
|
||||
<!-- from: content/send/materialiconsround/24px.svg -->
|
||||
<symbol id="icon-send" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M3.4 20.4l17.45-7.48c.81-.35.81-1.49 0-1.84L3.4 3.6c-.66-.29-1.39.2-1.39.91L2 9.12c0 .5.37.93.87.99L17 12 2.87 13.88c-.5.07-.87.5-.87 1l.01 4.61c0 .71.73 1.2 1.39.91z" />
|
||||
</symbol>
|
||||
<!-- from: file/file_download/materialicons/24px.svg -->
|
||||
<symbol id="icon-download" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z" />
|
||||
</symbol>
|
||||
<!-- from: file/file_upload/materialicons/24px.svg -->
|
||||
<symbol id="icon-upload" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z" />
|
||||
</symbol>
|
||||
<!-- from: file/folder/materialiconsround/24px.svg -->
|
||||
<symbol id="icon-folder" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M10.59 4.59C10.21 4.21 9.7 4 9.17 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-1.41-1.41z" />
|
||||
</symbol>
|
||||
<!-- from: navigation/arrow_back/materialiconsround/24px.svg -->
|
||||
<symbol id="icon-back" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
@@ -137,4 +173,9 @@ licensed under the terms of the Apache 2.0 License -->
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M17 7h-3c-.55 0-1 .45-1 1s.45 1 1 1h3c1.65 0 3 1.35 3 3s-1.35 3-3 3h-3c-.55 0-1 .45-1 1s.45 1 1 1h3c2.76 0 5-2.24 5-5s-2.24-5-5-5zm-9 5c0 .55.45 1 1 1h6c.55 0 1-.45 1-1s-.45-1-1-1H9c-.55 0-1 .45-1 1zm2 3H7c-1.65 0-3-1.35-3-3s1.35-3 3-3h3c.55 0 1-.45 1-1s-.45-1-1-1H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h3c.55 0 1-.45 1-1s-.45-1-1-1z" />
|
||||
</symbol>
|
||||
<!-- from: content/insights/materialiconsround/24px.svg -->
|
||||
<symbol id="icon-insights" viewBox="0 0 24 24">
|
||||
<g><rect fill="none" height="24" width="24" /><rect fill="none" height="24" width="24" /></g>
|
||||
<g><g><path d="M21,8c-1.45,0-2.26,1.44-1.93,2.51l-3.55,3.56c-0.3-0.09-0.74-0.09-1.04,0l-2.55-2.55C12.27,10.45,11.46,9,10,9 c-1.45,0-2.27,1.44-1.93,2.52l-4.56,4.55C2.44,15.74,1,16.55,1,18c0,1.1,0.9,2,2,2c1.45,0,2.26-1.44,1.93-2.51l4.55-4.56 c0.3,0.09,0.74,0.09,1.04,0l2.55,2.55C12.73,16.55,13.54,18,15,18c1.45,0,2.27-1.44,1.93-2.52l3.56-3.55 C21.56,12.26,23,11.45,23,10C23,8.9,22.1,8,21,8z" /><polygon points="15,9 15.94,6.93 18,6 15.94,5.07 15,3 14.08,5.07 12,6 14.08,6.93" /><polygon points="3.5,11 4,9 6,8.5 4,8 3.5,6 3,8 1,8.5 3,9" /></g></g>
|
||||
</symbol>
|
||||
</defs></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 18 KiB |
@@ -17,9 +17,12 @@
|
||||
<h3>{% trans %}Trademarks{% endtrans %}</h3>
|
||||
<p>{% trans trademarks_url="https://snikket.org/about/trademarks/" %}“Snikket” and the parrot logo are trademarks of Snikket Community Interest Company. For more information about the trademarks, visit the <a href="{{ trademarks_url }}">Snikket Trademarks information page</a>.{% endtrans %}
|
||||
<h3>{% trans %}Software Versions{% endtrans %}</h3>
|
||||
<pre>Snikket Server
|
||||
Domain: {{ config["SNIKKET_DOMAIN"] }}
|
||||
Snikket Web Portal{% if version %} ({{ version }}){% endif %}
|
||||
<pre>Domain: {{ config["SNIKKET_DOMAIN"] }}
|
||||
Web Portal{% if version %} ({{ version }}){% endif %}
|
||||
{%- if core_versions -%}
|
||||
{% for name, version in core_versions.items() %}
|
||||
{{ name }} ({{ version }}){% endfor %}
|
||||
{%- endif -%}
|
||||
{%- if extra_versions -%}
|
||||
{% for name, version in extra_versions.items() %}
|
||||
{{ name }} ({{ version }}){% endfor %}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<p>{% trans %}The user and their data will be deleted irrevocably, permanently and immediately upon pushing the below button. <strong>There is no way back!</strong>{% endtrans %}</p>
|
||||
{% endcall %}
|
||||
<div class="f-bbox">
|
||||
{%- call standard_button("back", url_for(".index"), class="secondary") %}{% trans %}Back{% endtrans %}{% endcall -%}
|
||||
{%- call standard_button("back", url_for(".edit_user", localpart=target_user.localpart), class="tertiary") %}{% trans %}Back{% endtrans %}{% endcall -%}
|
||||
{%- call form_button("delete", form.action_delete, class="primary danger") %}{% endcall -%}
|
||||
</div>
|
||||
</form></div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "admin_app.html" %}
|
||||
{% from "library.j2" import form_button, standard_button, value_or_hint, custom_form_button, clipboard_button %}
|
||||
{% from "library.j2" import form_button, standard_button, value_or_hint, custom_form_button, clipboard_button, icon %}
|
||||
{% block head_lead %}
|
||||
{{ super() }}
|
||||
{% include "copy-snippet.html" %}
|
||||
@@ -40,8 +40,8 @@
|
||||
{%- endif -%}
|
||||
</div>
|
||||
<div class="f-bbox">
|
||||
{%- call standard_button("back", url_for(".circles"), class="secondary") -%}
|
||||
{% trans %}Back{% endtrans %}
|
||||
{%- call standard_button("back", url_for(".circles"), class="tertiary") -%}
|
||||
{% trans %}Return to circle list{% endtrans %}
|
||||
{%- endcall -%}
|
||||
{%- call form_button("done", form.action_save, class="primary") %}{% endcall -%}
|
||||
</div>
|
||||
@@ -56,14 +56,21 @@
|
||||
{%- if circle_members -%}
|
||||
<div class="el-2 elevated"><table>
|
||||
<thead>
|
||||
<th>Login name</th>
|
||||
<th class="collapsible">Display name</th>
|
||||
<th>Actions</th>
|
||||
<th>{% trans %}Login name{% endtrans %}</th>
|
||||
<th class="collapsible">{% trans %}Display name{% endtrans %}</th>
|
||||
<th>{% trans %}Actions{% endtrans %}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for member in circle_members -%}
|
||||
{%- for localpart, member in circle_members -%}
|
||||
<tr>
|
||||
<td>{{ member.localpart }}</td>
|
||||
<td>
|
||||
{%- if member -%}
|
||||
{{ localpart }}
|
||||
{%- else -%}
|
||||
{{ localpart }}
|
||||
<span class="with-tooltip above" data-tooltip="{% trans %}The user has been deleted from the server.{% endtrans %}"><em> ({% trans %}deleted{% endtrans %})</em></span>
|
||||
{%- endif -%}
|
||||
</td>
|
||||
<td class="collapsible">{% call value_or_hint(member.display_name) %}{% endcall %}</td>
|
||||
<td class="nowrap">
|
||||
{%- call custom_form_button("remove_user", form.action_remove_user.name, member.localpart, class="primary danger", slim=True) -%}
|
||||
|
||||
@@ -44,10 +44,10 @@
|
||||
<dd>{{ invite.created_at | format_date }}</dd>
|
||||
</dl>
|
||||
<div class="f-bbox">
|
||||
{%- call form_button("remove_link", form.action_revoke, class="secondary danger") %}{% endcall -%}
|
||||
{%- call standard_button("back", url_for(".invitations"), class="primary") %}
|
||||
{% trans %}Back{% endtrans %}
|
||||
{%- call standard_button("back", url_for(".invitations"), class="tertiary") %}
|
||||
{% trans %}Return to invitation list{% endtrans %}
|
||||
{%- endcall %}
|
||||
{%- call form_button("remove_link", form.action_revoke, class="primary danger") %}{% endcall -%}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
78
snikket_web/templates/admin_edit_user.html
Normal file
78
snikket_web/templates/admin_edit_user.html
Normal file
@@ -0,0 +1,78 @@
|
||||
{% extends "admin_app.html" %}
|
||||
{% from "library.j2" import box, form_button, standard_button, icon %}
|
||||
{% macro access_level_description(role, caller=None) %}
|
||||
{%- if role == "prosody:restricted" -%}
|
||||
{% trans %}Limited users can interact with users on the same Snikket service and be members of circles.{% endtrans %}
|
||||
{%- elif role == "prosody:normal" -%}
|
||||
{% trans %}Like limited users and can also interact with users on other Snikket services.{% endtrans %}
|
||||
{%- elif role == "prosody:admin" -%}
|
||||
{% trans %}Like normal users and can access the admin panel in the web portal.{% endtrans %}
|
||||
{%- endif -%}
|
||||
{% endmacro %}
|
||||
{% macro access_level_icon(role, caller=None) %}
|
||||
{%- if role == "prosody:restricted" -%}
|
||||
{% call icon("lock") %}{% endcall %}
|
||||
{%- elif role == "prosody:admin" -%}
|
||||
{% call icon("admin") %}{% endcall %}
|
||||
{%- endif -%}
|
||||
{% endmacro %}
|
||||
{% block content %}
|
||||
<h1>{% trans user_name=target_user.localpart %}Edit user {{ user_name }}{% endtrans %}</h1>
|
||||
<form method="POST">{{ form.csrf_token }}<div class="form layout-expanded">
|
||||
<h2 class="form-title">{% trans %}Edit user{% endtrans %}</h2>
|
||||
<div class="f-ebox">
|
||||
{{ form.localpart.label }}
|
||||
{{ form.localpart(readonly="readonly") }}
|
||||
<p class="form-desc weak">{% trans %}The login name cannot be changed.{% endtrans %}</p>
|
||||
</div>
|
||||
<div class="f-ebox">
|
||||
{{ form.display_name.label }}
|
||||
{{ form.display_name }}
|
||||
</div>
|
||||
<h3 class="form-title">{% trans %}Access Level{% endtrans %}</h3>
|
||||
<p class="form-descr weak">{% trans %}The access level of a user determines what interactions are allowed for them on your Snikket service.{% endtrans %}</p>
|
||||
<div class="f-ebox">
|
||||
<fieldset>{#- -#}
|
||||
<legend class="a11y-only">{{ form.role.label.text }}</legend>
|
||||
{%- for level in form.role -%}
|
||||
<div class="radio-button-ext">
|
||||
{{ level }}<label for="{{ level.id }}">
|
||||
{%- trans title=level.label.text, icon=access_level_icon(level.data), description=access_level_description(level.data) -%}
|
||||
<strong>{{ title }}{{ icon }}</strong><p>{{ description }}</p>
|
||||
{%- endtrans -%}
|
||||
</label>
|
||||
</div>
|
||||
{%- endfor -%}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="f-bbox">
|
||||
{%- call standard_button("back", url_for(".users"), class="tertiary") -%}
|
||||
{%- trans -%}Return to user list{%- endtrans -%}
|
||||
{%- endcall -%}
|
||||
{%- call standard_button("delete", url_for(".delete_user", localpart=target_user.localpart), class="secondary") -%}
|
||||
{%- trans -%}Delete user{%- endtrans -%}
|
||||
{%- endcall -%}
|
||||
{%- call form_button("done", form.action_save, class="primary") %}{% endcall -%}
|
||||
</div>
|
||||
</div>
|
||||
<h2>{% trans %}Further actions{% endtrans %}</h2>
|
||||
<div class="form layout-expanded">
|
||||
<h2 class="form-title">{% trans %}Reset password{% endtrans %}</h2>
|
||||
{{ form.csrf_token }}
|
||||
<p class="form-desc">
|
||||
{% trans %}If the user has lost their password, you can use the button below to create a special link which allows to change the password of the account, once.{% endtrans %}
|
||||
</p>
|
||||
<div class="f-bbox">
|
||||
{%- call form_button("passwd", form.action_create_reset, class="primary") -%}{%- endcall -%}
|
||||
</div>
|
||||
<h2 class="form-title">{% trans %}Debug information{% endtrans %}</h2>
|
||||
<p class="form-desc">
|
||||
{% trans %}In some cases, extended information about the user account and the connected devices is necessary to troubleshoot issues. The button below reveals this (sensitive) information.{% endtrans %}
|
||||
</p>
|
||||
<div class="f-bbox">
|
||||
{%- call standard_button("bug_report", url_for(".debug_user", localpart=target_user.localpart), class="primary") -%}
|
||||
{%- trans -%}Show debug information{%- endtrans -%}
|
||||
{%- endcall -%}
|
||||
</div>
|
||||
</div></form>
|
||||
{% endblock %}
|
||||
@@ -31,6 +31,18 @@
|
||||
<div>{% call standard_button("link", url_for(".invitations"), class="primary") %}{% trans %}Manage invitations{% endtrans %}{% endcall %}</div>
|
||||
{#- -#}
|
||||
</li>
|
||||
<li>
|
||||
<h2>{% trans %}System health{% endtrans %}</h2>
|
||||
{#- -#}
|
||||
{%- if show_metrics -%}
|
||||
<p>{% trans %}View the server status or send a broadcast message to all users.{% endtrans %}</p>
|
||||
{%- else -%}
|
||||
<p>{% trans %}Send a broadcast message to all users.{% endtrans %}</p>
|
||||
{%- endif -%}
|
||||
{#- -#}
|
||||
<div>{% call standard_button("insights", url_for(".system"), class="primary") %}{% trans %}Manage system{% endtrans %}{% endcall %}</div>
|
||||
{#- -#}
|
||||
</li>
|
||||
<li>
|
||||
{#- -#}
|
||||
<p>{% trans %}Go back to your user's web portal page.{% endtrans %}</p>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<form method="POST">
|
||||
{{- form.csrf_token -}}
|
||||
<div class="form layout-expanded">
|
||||
<h2 class="form-title">{% trans user_name=target_user.localpart %}Password reset link for {{ user_name }}{% endtrans %}</h2>
|
||||
<h2 class="form-title">{% trans user_name=localpart %}Password reset link for {{ user_name }}{% endtrans %}</h2>
|
||||
<p class="form-desc">{% trans %}The following link will allow the user to reset their password on their device, once.{% endtrans %}</p>
|
||||
<dd>
|
||||
<dt>{% trans %}Valid until{% endtrans %}</dt>
|
||||
@@ -21,7 +21,7 @@
|
||||
{%- call custom_form_button("remove_link", form.action_revoke.name, reset_link.id_, class="secondary danger") -%}
|
||||
{% trans %}Destroy link{% endtrans %}
|
||||
{%- endcall -%}
|
||||
{%- call standard_button("back", url_for(".users"), class="primary") -%}
|
||||
{%- call standard_button("back", url_for(".edit_user", localpart=localpart), class="primary") -%}
|
||||
{% trans %}Back{% endtrans %}
|
||||
{%- endcall -%}
|
||||
</div>
|
||||
|
||||
105
snikket_web/templates/admin_system.html
Normal file
105
snikket_web/templates/admin_system.html
Normal file
@@ -0,0 +1,105 @@
|
||||
{% extends "admin_app.html" %}
|
||||
{% from "library.j2" import form_button %}
|
||||
{% block content %}
|
||||
<h1>{% trans %}Manage system{% endtrans %}</h1>
|
||||
{% if show_metrics %}
|
||||
<h2>{% trans %}Overall system status{% endtrans %}</h2>
|
||||
<div class="elevated el-2">
|
||||
<dl>
|
||||
<dt>{% trans %}System load (5 minute average){% endtrans %}</dt>
|
||||
<dd>
|
||||
{%- if metrics.load5 -%}
|
||||
{{ metrics.load5 }}
|
||||
{%- else -%}
|
||||
<em>{% trans %}unknown{% endtrans %}</em>
|
||||
{%- endif -%}
|
||||
</dd>
|
||||
<dt>{% trans %}Memory use{% endtrans %}</dt>
|
||||
<dd>
|
||||
{%- if metrics.mem_total and metrics.mem_available -%}
|
||||
{% trans percentage_global=((1 - (metrics.mem_available / metrics.mem_total)) | format_percent), percentage_snikket=((((metrics.prosody_rss | default(0)) + (metrics.portal_rss | default(0))) / metrics.mem_total) | format_percent), mem_available=(metrics.mem_total | format_bytes) %}{{ percentage_global }} of {{ mem_available }}. Of that, Snikket uses {{ percentage_snikket }}.{% endtrans %}
|
||||
{%- else -%}
|
||||
<em>{% trans %}unknown{% endtrans %}</em>
|
||||
{%- endif -%}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<h2>{% trans %}Web portal status{% endtrans %}</h2>
|
||||
<div class="elevated el-2">
|
||||
<dl>
|
||||
<dt>{% trans %}Version{% endtrans %}</dt>
|
||||
<dd>{{ version }} <a href="{{ url_for("main.about") }}">{% trans %}View all versions{% endtrans %}</a></dd>
|
||||
<dt>{% trans %}Average CPU use{% endtrans %}</dt>
|
||||
<dd>
|
||||
{%- if metrics.portal_cpu -%}
|
||||
{{ metrics.portal_cpu | format_percent }}
|
||||
{%- else -%}
|
||||
<em>{% trans %}unknown{% endtrans %}</em>
|
||||
{%- endif -%}
|
||||
</dd>
|
||||
<dt>{% trans %}Current memory use{% endtrans %}</dt>
|
||||
<dd>
|
||||
{%- if metrics.portal_rss -%}
|
||||
{{ metrics.portal_rss | format_bytes }}
|
||||
{%- else -%}
|
||||
<em>{% trans %}unknown{% endtrans %}</em>
|
||||
{%- endif -%}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<h2>{% trans %}Snikket server status{% endtrans %}</h2>
|
||||
<div class="elevated el-2">
|
||||
<dl>
|
||||
<dt>{% trans %}Version{% endtrans %}</dt>
|
||||
<dd>{{ prosody_version }} <a href="{{ url_for("main.about") }}">{% trans %}View all versions{% endtrans %}</a></dd>
|
||||
<dt>{% trans %}Average CPU use{% endtrans %}</dt>
|
||||
<dd>
|
||||
{%- if metrics.prosody_cpu -%}
|
||||
{{ metrics.prosody_cpu | format_percent }}
|
||||
{%- else -%}
|
||||
<em>{% trans %}unknown{% endtrans %}</em>
|
||||
{%- endif -%}
|
||||
</dd>
|
||||
<dt>{% trans %}Current memory use{% endtrans %}</dt>
|
||||
<dd>
|
||||
{%- if metrics.prosody_rss -%}
|
||||
{{ metrics.prosody_rss | format_bytes }}
|
||||
{%- else -%}
|
||||
<em>{% trans %}unknown{% endtrans %}</em>
|
||||
{%- endif -%}
|
||||
</dd>
|
||||
<dt>{% trans %}Storage used by shared files{% endtrans %}</dt>
|
||||
<dd>
|
||||
{%- if metrics.prosody_uploads | default(None) is not none -%}
|
||||
{{ metrics.prosody_uploads | format_bytes }}
|
||||
{%- else -%}
|
||||
<em>{% trans %}unknown{% endtrans %}</em>
|
||||
{%- endif -%}
|
||||
</dd>
|
||||
<dt>{% trans %}Connected devices{% endtrans %}</dt>
|
||||
<dd>
|
||||
{%- if metrics.prosody_devices | default(None) is not none -%}
|
||||
{{ metrics.prosody_devices }}
|
||||
{%- else -%}
|
||||
<em>{% trans %}unknown{% endtrans %}</em>
|
||||
{%- endif -%}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h2>{% trans %}Broadcast message{% endtrans %}</h2>
|
||||
<form method="POST">{{ form.csrf_token }}<div class="form layout-expanded">
|
||||
<p class="form-desc">{% trans %}This form allows you to send a message to all users currently online on your Snikket server. Use it wisely.{% endtrans %}</p>
|
||||
<div class="f-ebox">
|
||||
{{ form.text.label }}
|
||||
{{ form.text }}
|
||||
</div>
|
||||
<div class="f-ebox">
|
||||
{{ form.online_only }}{{ form.online_only.label }}
|
||||
</div>
|
||||
<div class="f-bbox">
|
||||
{%- call form_button("send", form.action_send_preview, class="primary") -%}{%- endcall -%}
|
||||
{%- call form_button("broadcast", form.action_post_all, class="secondary accent") -%}{%- endcall -%}
|
||||
</div>
|
||||
</div></form>
|
||||
{% endblock %}
|
||||
@@ -1,9 +1,7 @@
|
||||
{% extends "admin_app.html" %}
|
||||
{% from "library.j2" import action_button, value_or_hint, custom_form_button %}
|
||||
{% from "library.j2" import action_button, icon, value_or_hint, custom_form_button %}
|
||||
{% block content %}
|
||||
<h1>{% trans %}Manage users{% endtrans %}</h1>
|
||||
<form method="POST" action="{{ url_for(".create_password_reset_link") }}">
|
||||
{{- reset_form.csrf_token -}}
|
||||
<div class="elevated el-2"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -15,17 +13,19 @@
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{ user.localpart }}</td>
|
||||
<td>
|
||||
{{- user.localpart -}}
|
||||
{%- if user.has_admin_role -%}
|
||||
<span class="with-tooltip above" data-tooltip="{% trans %}The user is an administrator.{% endtrans %}">{% call icon("admin") %}{% trans %} (Administrator){% endtrans %}{% endcall %}</span>
|
||||
{%- endif -%}
|
||||
{%- if user.has_restricted_role -%}
|
||||
<span class="with-tooltip above" data-tooltip="{% trans %}The user is restricted.{% endtrans %}">{% call icon("lock") %}{% trans %} (Restricted){% endtrans %}{% endcall %}</span>
|
||||
{%- endif -%}
|
||||
</td>
|
||||
<td>{% call value_or_hint(user.display_name) %}{% endcall %}</td>
|
||||
<td class="nowrap">
|
||||
{%- call action_button("delete", url_for(".delete_user", localpart=user.localpart), class="secondary") -%}
|
||||
{% trans user_name=user.localpart %}Delete user {{ user_name }}{% endtrans %}
|
||||
{%- endcall -%}
|
||||
{%- call action_button("bug_report", url_for(".debug_user", localpart=user.localpart), class="secondary") -%}
|
||||
{% trans user_name=user.localpart %}Show debug information for {{ user_name }}{% endtrans %}
|
||||
{%- endcall -%}
|
||||
{%- call custom_form_button("passwd", reset_form.action_create.name, user.localpart, class="secondary", slim=True) -%}
|
||||
{% trans user_name=user.localpart %}Create password reset link for {{ user_name }}{% endtrans %}
|
||||
{%- call action_button("edit", url_for(".edit_user", localpart=user.localpart), class="primary") -%}
|
||||
{% trans user_name=user.localpart %}Edit user {{ user_name }}{% endtrans %}
|
||||
{%- endcall -%}
|
||||
</form>
|
||||
</td>
|
||||
@@ -33,6 +33,5 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table></div>
|
||||
</form>
|
||||
{%- include "admin_create_invite_form.html" -%}
|
||||
{% endblock %}
|
||||
|
||||
@@ -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 -%}
|
||||
|
||||
@@ -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 -%}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "invite.html" %}
|
||||
{% set body_id = "invite" %}
|
||||
{% from "library.j2" import form_button, clipboard_button %}
|
||||
{% from "library.j2" import form_button, clipboard_button, render_errors %}
|
||||
{% block head_lead %}
|
||||
<title>{% trans site_name=config["SITE_NAME"] %}Successfully registered on {{ site_name }} | Snikket{% endtrans %}</title>
|
||||
{%- include "copy-snippet.html" -%}
|
||||
@@ -15,6 +15,47 @@
|
||||
{% trans %}Copy address{% endtrans %}
|
||||
{%- endcall -%}
|
||||
<p>{% trans %}You can now set up your legacy XMPP client with the above address and the password you chose during registration.{% endtrans %}</p>
|
||||
<p>{% trans %}You can now safely close this page.{% endtrans %}</p>
|
||||
<p>{% trans login_url=url_for('main.login') %}You can now safely close this page, or log in to the web portal to <a href="{{ login_url }}">manage your account</a>.{% endtrans %}</p>
|
||||
|
||||
{% if migration_success %}
|
||||
<h2>{% trans %}Import successful{% endtrans %}</h2>
|
||||
<p>{% trans %}Congratulations! Your account data has been successfully imported.{% endtrans %}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if form %}
|
||||
<h2>{% trans %}Moving to Snikket?{% endtrans %}</h2>
|
||||
<p>{% trans %}If you are moving from a different Snikket instance or another XMPP-compatible service, you may optionally import the data (contacts, profile information, etc.) from your previous account. When you have exported the data from your previous account, upload it using the form below.{% endtrans %}</p>
|
||||
|
||||
<div class="form layout-expanded"><form method="POST" enctype="multipart/form-data">
|
||||
<h3 class="form-title">{% trans %}Upload account data{% endtrans %}</h3>
|
||||
{{ form.csrf_token }}
|
||||
{% call render_errors(form) %}{% endcall %}
|
||||
<div class="f-ebox">
|
||||
{{ form.account_data_file.label }}
|
||||
{{ form.account_data_file(accept="application/xml",
|
||||
data_maxsize=max_import_size,
|
||||
data_warning_header=import_too_big_warning_header,
|
||||
data_maxsize_warning=import_too_big_warning) }}
|
||||
</div>
|
||||
<div class="f-bbox">
|
||||
{%- call form_button("upload", form.action_import, class="secondary") %}{% endcall -%}
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
document.getElementById("{{ form.account_data_file.id }}").onchange = function() {
|
||||
var maxsize_s = this.dataset.maxsize;
|
||||
var maxsize = parseInt(maxsize_s);
|
||||
if (this.files[0].size > maxsize) {
|
||||
var warning_header = this.dataset.warningHeader;
|
||||
var warning_text = this.dataset.maxsizeWarning;
|
||||
this.setCustomValidity(warning_text);
|
||||
this.reportValidity();
|
||||
this.value = null;
|
||||
} else {
|
||||
this.setCustomValidity("");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</form></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -56,29 +56,7 @@
|
||||
{%- endcall -%}
|
||||
</header>
|
||||
<p>{% trans %}You can transfer this invite to your mobile device by scanning a code with your camera. You can use either a QR scanner app or the Snikket app itself.{% endtrans %}</p>
|
||||
<div class="tabbox">
|
||||
{#- -#}
|
||||
<nav class="tabs" role="tablist">
|
||||
{#- -#}
|
||||
<a href="#qr-info-url" class="active" role="tab" aria-controls="qr-info-url" aria-selected="true" onclick="select_tab(this); return false;">{% trans %}Using a QR code scanner{% endtrans %}</a>
|
||||
{#- -#}
|
||||
<a href="#qr-info-uri" role="tab" aria-controls="qr-info-uri" aria-selected="false" onclick="select_tab(this); return false;">{% trans %}Using the Snikket app{% endtrans %}</a>
|
||||
{#- -#}
|
||||
</nav>
|
||||
{#- -#}
|
||||
<div id="qr-info-url" class="tab-pane active">
|
||||
<p>{% trans %}Use a <em>QR code</em> scanner on your mobile device to scan the code below:{% endtrans %}</p>
|
||||
<div id="qr-invite-page" data-qrdata="{{ url_for(".view", id_=invite_id, _external=True) }}" class="qr"></div>
|
||||
</div>
|
||||
{#- -#}
|
||||
<div id="qr-info-uri" class="tab-pane">
|
||||
<img class="float-right" id="tutorial-scan" aria-hidden="true" alt="" src="{{ url_for("static", filename="img/tutorial-scan.png") }}">
|
||||
<p>{% trans %}Install the Snikket app on your mobile device, open it, and tap the 'Scan' button at the top.{% endtrans %}</p>
|
||||
<p>{% trans %}Your camera will turn on. Point it at the square code below until it is within the highlighted square on your screen, and wait until the app recognises it.{% endtrans %}</p>
|
||||
<div id="qr-uri" data-qrdata="{{ invite.xmpp_uri }}" class="qr"></div>
|
||||
</div>
|
||||
{#- -#}
|
||||
</div>
|
||||
<div id="qr-invite-page" data-qrdata="{{ url_for(".view", id_=invite_id, _external=True, _scheme="https") }}" class="qr"></div>
|
||||
{#- -#}
|
||||
{%- call standard_button("close", "#", onclick="close_modal(this.parentNode.parentNode); return false;", class="primary") -%}
|
||||
{% trans %}Close{% endtrans %}
|
||||
@@ -96,9 +74,9 @@
|
||||
{% trans %}Close{% endtrans %}
|
||||
{%- endcall -%}
|
||||
</header>
|
||||
<p>{% trans %}After downloading Snikket from the app store, you have to return to this invite link and tap on "Open the app" to proceed.{% endtrans %}</p>
|
||||
<p>{% trans %}After downloading Snikket from the App Store, you have to return to this invite link and tap on "Open the app" to proceed.{% endtrans %}</p>
|
||||
<ol>
|
||||
<li><p>{% trans %}First download Snikket from the app store using the button below:{% endtrans %}</p>
|
||||
<li><p>{% trans %}First download Snikket from the App Store using the button below:{% endtrans %}</p>
|
||||
<p><a href="{{ apple_store_url }}"><img alt='{% trans %}Download on the App Store{% endtrans %}' src="{{ apple_store_badge() }}" class="apple"></a></p>
|
||||
<li><p>{% trans %}After the installation is complete, you can return to this page and tap the "Open the app" button to continue with the setup:{% endtrans %}</p>
|
||||
<p>
|
||||
@@ -127,7 +105,7 @@
|
||||
<p>{% trans %}After installing Snikket via F-Droid, you have to return to this invite link and tap on "Open the app" to proceed.{% endtrans %}</p>
|
||||
<ol>
|
||||
<li><p>{% trans %}First install Snikket from F-Droid using the button below:{% endtrans %}</p>
|
||||
<p><a href="{{ f_droid_url }}" class="popover" data-popover-id="fdroid-popover"><img alt='{% trans %}Install via F-Droid{% endtrans %}' src='{{ url_for('static', filename='img/f-droid-badge.png') }}' class="fdroid"/></a></p></li>
|
||||
<p><a href="{{ f_droid_url }}"><img alt='{% trans %}Install via F-Droid{% endtrans %}' src='{{ url_for('static', filename='img/f-droid-badge.png') }}' class="fdroid"/></a></p></li>
|
||||
<li><p>{% trans %}After the installation is complete, you can return to this page and tap the "Open the app" button to continue with the setup:{% endtrans %}</p>
|
||||
<p>
|
||||
{%- call standard_button("exit_to_app", invite.xmpp_uri, class="primary") -%}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
<div>
|
||||
<div>{% call standard_button("edit", url_for(".profile"), class="primary") %}{% trans %}Edit profile{% endtrans %}{% endcall %}</div>
|
||||
<div>{% call standard_button("passwd", url_for(".change_pw"), class="secondary") %}{% trans %}Change password{% endtrans %}{% endcall %}</div>
|
||||
<div>{% call standard_button("folder", url_for(".manage_data"), class="secondary") %}{% trans %}Manage your data{% endtrans %}{% endcall %}</div>
|
||||
</div>
|
||||
{#- -#}
|
||||
</li>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<p class="form-desc">{% trans %}Click below to log yourself out of the web portal. This does not affect any other connected devices.{% endtrans %}</p>
|
||||
{{ form.csrf_token }}
|
||||
<div class="f-bbox">
|
||||
{%- call standard_button("back", url_for("user.index"), class="secondary") -%}
|
||||
{%- call standard_button("back", url_for("user.index"), class="tertiary") -%}
|
||||
{% trans %}Back{% endtrans %}
|
||||
{%- endcall -%}
|
||||
{%- call form_button("logout", form.action_signout, class="primary") %}{% endcall -%}
|
||||
|
||||
24
snikket_web/templates/user_manage_data.html
Normal file
24
snikket_web/templates/user_manage_data.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends "app.html" %}
|
||||
{% from "library.j2" import standard_button, form_button, render_errors, avatar with context %}
|
||||
{% block content %}
|
||||
<h1>{% trans %}Manage your data{% endtrans %}</h1>
|
||||
<nav class="welcome">
|
||||
<ul>
|
||||
<li>
|
||||
<h2>{% trans %}Export account{% endtrans %}</h2>
|
||||
<p>{% trans %}Download your account data as a file for backup purposes or to move your account to another service.{% endtrans %}</p>
|
||||
|
||||
{% call render_errors(form) %}{% endcall %}
|
||||
|
||||
<div class="f-bbox">
|
||||
{%- call standard_button("back", url_for('.index'), class="tertiary") %}{% trans %}Back{% endtrans %}{% endcall -%}
|
||||
|
||||
<form method="POST">
|
||||
{{ form.csrf_token }}
|
||||
{%- call form_button("download", form.action_export, class="primary") %}{% endcall -%}
|
||||
</form>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
@@ -9,22 +9,22 @@
|
||||
{%- 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>
|
||||
<p>{% trans %}After changing your password, you will have to enter the new password on all of your devices.{% endtrans %}</p>
|
||||
</div>
|
||||
<div class="f-bbox">
|
||||
{%- call standard_button("back", url_for('.index'), class="secondary") %}{% trans %}Back{% endtrans %}{% endcall -%}
|
||||
{%- call standard_button("back", url_for('.index'), class="tertiary") %}{% trans %}Back{% endtrans %}{% endcall -%}
|
||||
{%- call custom_form_button("passwd", "", "", class="primary") -%}
|
||||
{% trans %}Change password{% endtrans %}
|
||||
{%- endcall -%}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="f-bbox">
|
||||
{%- call standard_button("back", url_for('.index'), class="secondary") %}{% trans %}Back{% endtrans %}{% endcall -%}
|
||||
{%- call standard_button("back", url_for('.index'), class="tertiary") %}{% trans %}Back{% endtrans %}{% endcall -%}
|
||||
{%- call form_button("done", form.action_save, class="primary") %}{% endcall -%}
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
|
||||
BIN
snikket_web/translations/da/LC_MESSAGES/messages.mo
Normal file
BIN
snikket_web/translations/da/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
snikket_web/translations/en/LC_MESSAGES/messages.mo
Normal file
BIN
snikket_web/translations/en/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
snikket_web/translations/en_GB/LC_MESSAGES/messages.mo
Normal file
BIN
snikket_web/translations/en_GB/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
snikket_web/translations/es_MX/LC_MESSAGES/messages.mo
Normal file
BIN
snikket_web/translations/es_MX/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
snikket_web/translations/id/LC_MESSAGES/messages.mo
Normal file
BIN
snikket_web/translations/id/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
snikket_web/translations/it/LC_MESSAGES/messages.mo
Normal file
BIN
snikket_web/translations/it/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,276 +1,373 @@
|
||||
# Translations template for PROJECT.
|
||||
# Copyright (C) 2021 ORGANIZATION
|
||||
# Copyright (C) 2023 ORGANIZATION
|
||||
# This file is distributed under the same license as the PROJECT project.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2021.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2023.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2021-03-20 16:44+0100\n"
|
||||
"POT-Creation-Date: 2023-03-28 19:16+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.9.0\n"
|
||||
"Generated-By: Babel 2.12.1\n"
|
||||
|
||||
#: snikket_web/admin.py:59
|
||||
msgid "Delete user permanently"
|
||||
#: snikket_web/admin.py:69 snikket_web/templates/admin_delete_user.html:10
|
||||
#: snikket_web/templates/admin_edit_circle.html:59
|
||||
#: snikket_web/templates/admin_users.html:8
|
||||
msgid "Login name"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:72
|
||||
msgid "User deleted"
|
||||
#: snikket_web/admin.py:73 snikket_web/templates/admin_delete_user.html:12
|
||||
#: snikket_web/templates/admin_edit_circle.html:60
|
||||
#: snikket_web/templates/admin_users.html:9 snikket_web/user.py:63
|
||||
msgid "Display name"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:115
|
||||
#: snikket_web/admin.py:77 snikket_web/templates/admin_edit_user.html:32
|
||||
msgid "Access Level"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:79
|
||||
msgid "Limited"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:80
|
||||
msgid "Normal user"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:81
|
||||
msgid "Administrator"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:86
|
||||
msgid "Update user"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:90
|
||||
msgid "Create password reset link"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:108
|
||||
msgid "Password reset link created"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:121
|
||||
#: snikket_web/admin.py:123
|
||||
msgid "User information updated."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:145
|
||||
msgid "Delete user permanently"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:158
|
||||
msgid "User deleted"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:196
|
||||
msgid "Password reset link not found"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:208
|
||||
msgid "Password reset link deleted"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:140
|
||||
#: snikket_web/admin.py:228
|
||||
msgid "Invite to circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:146
|
||||
#: snikket_web/admin.py:234
|
||||
msgid "At least one circle must be selected"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:151
|
||||
#: snikket_web/admin.py:239
|
||||
msgid "Valid for"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:153
|
||||
#: snikket_web/admin.py:241
|
||||
msgid "One hour"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:154
|
||||
#: snikket_web/admin.py:242
|
||||
msgid "Twelve hours"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:155
|
||||
#: snikket_web/admin.py:243
|
||||
msgid "One day"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:156
|
||||
#: snikket_web/admin.py:244
|
||||
msgid "One week"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:157
|
||||
#: snikket_web/admin.py:245
|
||||
msgid "Four weeks"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:163 snikket_web/templates/admin_edit_invite.html:17
|
||||
#: snikket_web/admin.py:251 snikket_web/templates/admin_edit_invite.html:17
|
||||
msgid "Invitation type"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:165 snikket_web/templates/library.j2:116
|
||||
#: snikket_web/admin.py:253 snikket_web/templates/library.j2:116
|
||||
msgid "Individual"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:166 snikket_web/templates/library.j2:114
|
||||
#: snikket_web/admin.py:254 snikket_web/templates/library.j2:114
|
||||
msgid "Group"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:172
|
||||
#: snikket_web/admin.py:260
|
||||
msgid "New invitation link"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:234
|
||||
#: snikket_web/admin.py:322
|
||||
msgid "Revoke"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:258
|
||||
#: snikket_web/admin.py:346
|
||||
msgid "Invitation created"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:274
|
||||
#: snikket_web/admin.py:362
|
||||
msgid "No such invitation exists"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:289
|
||||
#: snikket_web/admin.py:377
|
||||
msgid "Invitation revoked"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:306 snikket_web/admin.py:354
|
||||
#: snikket_web/admin.py:394 snikket_web/admin.py:442
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:311 snikket_web/templates/admin_circles.html:47
|
||||
#: snikket_web/admin.py:399 snikket_web/templates/admin_circles.html:47
|
||||
msgid "Create circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:341
|
||||
#: snikket_web/admin.py:429
|
||||
msgid "Circle created"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:359
|
||||
#: snikket_web/admin.py:447
|
||||
msgid "Select user"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:364
|
||||
#: snikket_web/admin.py:452
|
||||
msgid "Update circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:368
|
||||
#: snikket_web/admin.py:456
|
||||
msgid "Delete circle permanently"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:374
|
||||
#: snikket_web/admin.py:462
|
||||
msgid "Add user"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:390
|
||||
#: snikket_web/admin.py:478
|
||||
msgid "No such circle exists"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:427
|
||||
#: snikket_web/admin.py:515
|
||||
msgid "Circle data updated"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:433
|
||||
#: snikket_web/admin.py:521
|
||||
msgid "Circle deleted"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:444
|
||||
#: snikket_web/admin.py:532
|
||||
msgid "User added to circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:453
|
||||
#: snikket_web/admin.py:541
|
||||
msgid "User removed from circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/infra.py:41
|
||||
#: snikket_web/admin.py:610
|
||||
msgid "Message contents"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:616
|
||||
msgid "Only send to online users"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:620
|
||||
msgid "Post to all users"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:624
|
||||
msgid "Send preview to yourself"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:646
|
||||
msgid "Announcement sent!"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/infra.py:53
|
||||
msgid "Main"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/invite.py:106
|
||||
#: snikket_web/invite.py:35
|
||||
msgid ""
|
||||
"The account data you tried to import is too large to upload. Please "
|
||||
"contact your Snikket operator."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/invite.py:114
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/invite.py:110 snikket_web/invite.py:177 snikket_web/main.py:41
|
||||
#: snikket_web/invite.py:118 snikket_web/invite.py:186 snikket_web/main.py:43
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/invite.py:114 snikket_web/invite.py:181
|
||||
#: snikket_web/invite.py:122 snikket_web/invite.py:190
|
||||
msgid "Confirm password"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/invite.py:118 snikket_web/invite.py:185
|
||||
#: snikket_web/invite.py:126 snikket_web/invite.py:194
|
||||
msgid "The passwords must match."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/invite.py:123
|
||||
#: snikket_web/invite.py:131
|
||||
msgid "Create account"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/invite.py:150
|
||||
#: snikket_web/invite.py:158
|
||||
msgid "That username is already taken."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/invite.py:154 snikket_web/invite.py:218
|
||||
#: snikket_web/invite.py:162 snikket_web/invite.py:227
|
||||
msgid "Registration was declined for unknown reasons."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/invite.py:158
|
||||
#: snikket_web/invite.py:166
|
||||
msgid "The username is not valid."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/invite.py:190 snikket_web/templates/user_home.html:32
|
||||
#: snikket_web/invite.py:199 snikket_web/templates/user_home.html:32
|
||||
#: snikket_web/templates/user_passwd.html:29
|
||||
msgid "Change password"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/main.py:36
|
||||
#: snikket_web/invite.py:246
|
||||
msgid "Account data file"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/invite.py:250
|
||||
msgid "Import data"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/invite.py:271
|
||||
#, python-format
|
||||
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 ""
|
||||
|
||||
#: snikket_web/invite.py:291 snikket_web/templates/unauth.html:18
|
||||
#: snikket_web/user.py:178
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/main.py:38
|
||||
msgid "Address"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/main.py:46
|
||||
#: snikket_web/main.py:48
|
||||
msgid "Sign in"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/main.py:55
|
||||
#: snikket_web/main.py:57
|
||||
msgid "Invalid username or password."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/main.py:83
|
||||
#: snikket_web/main.py:85
|
||||
msgid "Login successful!"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:27
|
||||
#: snikket_web/user.py:29
|
||||
msgid "Current password"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:32
|
||||
#: snikket_web/user.py:34
|
||||
msgid "New password"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:37
|
||||
#: snikket_web/user.py:39
|
||||
msgid "Confirm new password"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:41
|
||||
#: snikket_web/user.py:43
|
||||
msgid "The new passwords must match."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:48
|
||||
#: snikket_web/user.py:50
|
||||
msgid "Sign out"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:53
|
||||
#: snikket_web/user.py:55
|
||||
msgid "Nobody"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:54
|
||||
#: snikket_web/user.py:56
|
||||
msgid "Friends only"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:55
|
||||
#: snikket_web/user.py:57
|
||||
msgid "Everyone"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:12
|
||||
#: snikket_web/templates/admin_users.html:11 snikket_web/user.py:61
|
||||
msgid "Display name"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:65
|
||||
#: snikket_web/user.py:67
|
||||
msgid "Avatar"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:69
|
||||
#: snikket_web/user.py:71
|
||||
msgid "Profile visibility"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:74
|
||||
#: snikket_web/user.py:76
|
||||
msgid "Update profile"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:99
|
||||
msgid "Incorrect password."
|
||||
#: snikket_web/user.py:82
|
||||
msgid "Account data"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:103
|
||||
msgid "Password changed"
|
||||
#: snikket_web/user.py:86
|
||||
msgid "Upload"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:111
|
||||
msgid "Incorrect password."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:115
|
||||
msgid "Password changed"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:123
|
||||
msgid ""
|
||||
"The chosen avatar is too big. To be able to upload larger avatars, please"
|
||||
" use the app."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:159
|
||||
#: snikket_web/user.py:170
|
||||
msgid "Profile updated"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/unauth.html:18 snikket_web/user.py:167
|
||||
msgid "Error"
|
||||
#: snikket_web/user.py:184
|
||||
msgid "Export"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:202
|
||||
msgid "You currently have no account data to export."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/_footer.html:4
|
||||
@@ -348,7 +445,7 @@ msgstr ""
|
||||
msgid "Software Versions"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/about.html:29
|
||||
#: snikket_web/templates/about.html:32
|
||||
msgid "Back to the main page"
|
||||
msgstr ""
|
||||
|
||||
@@ -383,8 +480,9 @@ msgid "Members"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_circles.html:15
|
||||
#: snikket_web/templates/admin_edit_circle.html:61
|
||||
#: snikket_web/templates/admin_invites.html:24
|
||||
#: snikket_web/templates/admin_users.html:12
|
||||
#: snikket_web/templates/admin_users.html:10
|
||||
msgid "Actions"
|
||||
msgstr ""
|
||||
|
||||
@@ -455,12 +553,12 @@ msgid "Copy complete output"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:4
|
||||
#: snikket_web/templates/admin_users.html:22
|
||||
#, python-format
|
||||
msgid "Delete user %(user_name)s"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:6
|
||||
#: snikket_web/templates/admin_edit_user.html:53
|
||||
msgid "Delete user"
|
||||
msgstr ""
|
||||
|
||||
@@ -468,11 +566,6 @@ msgstr ""
|
||||
msgid "Are you sure you want to delete the following user?"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:10
|
||||
#: snikket_web/templates/admin_users.html:10
|
||||
msgid "Login name"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:15
|
||||
msgid "Danger"
|
||||
msgstr ""
|
||||
@@ -485,10 +578,9 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:19
|
||||
#: snikket_web/templates/admin_edit_circle.html:44
|
||||
#: snikket_web/templates/admin_edit_invite.html:49
|
||||
#: snikket_web/templates/admin_reset_user_password.html:25
|
||||
#: snikket_web/templates/user_logout.html:10
|
||||
#: snikket_web/templates/user_manage_data.html:14
|
||||
#: snikket_web/templates/user_passwd.html:27
|
||||
#: snikket_web/templates/user_profile.html:32
|
||||
msgid "Back"
|
||||
@@ -522,6 +614,10 @@ msgstr ""
|
||||
msgid "This circle has no group chat associated."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:44
|
||||
msgid "Return to circle list"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:48
|
||||
msgid "Delete circle"
|
||||
msgstr ""
|
||||
@@ -534,28 +630,37 @@ msgstr ""
|
||||
msgid "Circle members"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:70
|
||||
#: snikket_web/templates/admin_edit_circle.html:71
|
||||
msgid "The user has been deleted from the server."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:71
|
||||
#: snikket_web/templates/library.j2:108
|
||||
msgid "deleted"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:77
|
||||
#, python-format
|
||||
msgid "Remove user %(username)s from circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:78
|
||||
#: snikket_web/templates/admin_edit_circle.html:85
|
||||
msgid "This circle currently has no members."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:80
|
||||
#: snikket_web/templates/admin_edit_circle.html:87
|
||||
msgid "Invite more members"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:83
|
||||
#: snikket_web/templates/admin_edit_circle.html:90
|
||||
msgid "Add existing user"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:94
|
||||
#: snikket_web/templates/admin_edit_circle.html:101
|
||||
msgid "All users added"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:95
|
||||
#: snikket_web/templates/admin_edit_circle.html:102
|
||||
msgid "All users on this service are already in this circle."
|
||||
msgstr ""
|
||||
|
||||
@@ -604,6 +709,85 @@ msgstr ""
|
||||
msgid "Created"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_invite.html:48
|
||||
msgid "Return to invitation list"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:5
|
||||
msgid ""
|
||||
"Limited users can interact with users on the same Snikket service and be "
|
||||
"members of circles."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:7
|
||||
msgid ""
|
||||
"Like limited users and can also interact with users on other Snikket "
|
||||
"services."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:9
|
||||
msgid "Like normal users and can access the admin panel in the web portal."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:20
|
||||
#: snikket_web/templates/admin_users.html:28
|
||||
#, python-format
|
||||
msgid "Edit user %(user_name)s"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:22
|
||||
msgid "Edit user"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:26
|
||||
msgid "The login name cannot be changed."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:33
|
||||
msgid ""
|
||||
"The access level of a user determines what interactions are allowed for "
|
||||
"them on your Snikket service."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:40
|
||||
#, python-format
|
||||
msgid "<strong>%(title)s%(icon)s</strong><p>%(description)s</p>"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:50
|
||||
msgid "Return to user list"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:58
|
||||
msgid "Further actions"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:60
|
||||
msgid "Reset password"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:63
|
||||
msgid ""
|
||||
"If the user has lost their password, you can use the button below to "
|
||||
"create a special link which allows to change the password of the account,"
|
||||
" once."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:68
|
||||
msgid "Debug information"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:70
|
||||
msgid ""
|
||||
"In some cases, extended information about the user account and the "
|
||||
"connected devices is necessary to troubleshoot issues. The button below "
|
||||
"reveals this (sensitive) information."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:74
|
||||
msgid "Show debug information"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_home.html:4
|
||||
msgid "Welcome to the admin panel!"
|
||||
msgstr ""
|
||||
@@ -643,11 +827,28 @@ msgstr ""
|
||||
msgid "Manage invitations"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_home.html:36
|
||||
msgid "Go back to your user's web portal page."
|
||||
#: snikket_web/templates/admin_home.html:35
|
||||
msgid "System health"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_home.html:38
|
||||
msgid "View the server status or send a broadcast message to all users."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_home.html:40
|
||||
msgid "Send a broadcast message to all users."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_home.html:43
|
||||
#: snikket_web/templates/admin_system.html:4
|
||||
msgid "Manage system"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_home.html:48
|
||||
msgid "Go back to your user's web portal page."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_home.html:50
|
||||
msgid "Exit admin panel"
|
||||
msgstr ""
|
||||
|
||||
@@ -698,14 +899,96 @@ msgstr ""
|
||||
msgid "Destroy link"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_users.html:25
|
||||
#, python-format
|
||||
msgid "Show debug information for %(user_name)s"
|
||||
#: snikket_web/templates/admin_system.html:6
|
||||
msgid "Overall system status"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_users.html:28
|
||||
#: snikket_web/templates/admin_system.html:9
|
||||
msgid "System load (5 minute average)"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_system.html:14
|
||||
#: snikket_web/templates/admin_system.html:22
|
||||
#: snikket_web/templates/admin_system.html:37
|
||||
#: snikket_web/templates/admin_system.html:45
|
||||
#: snikket_web/templates/admin_system.html:60
|
||||
#: snikket_web/templates/admin_system.html:68
|
||||
#: snikket_web/templates/admin_system.html:76
|
||||
#: snikket_web/templates/admin_system.html:84
|
||||
msgid "unknown"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_system.html:17
|
||||
msgid "Memory use"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_system.html:20
|
||||
#, python-format
|
||||
msgid "Create password reset link for %(user_name)s"
|
||||
msgid ""
|
||||
"%(percentage_global)s of %(mem_available)s. Of that, Snikket uses "
|
||||
"%(percentage_snikket)s."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_system.html:27
|
||||
msgid "Web portal status"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_system.html:30
|
||||
#: snikket_web/templates/admin_system.html:53
|
||||
msgid "Version"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_system.html:31
|
||||
#: snikket_web/templates/admin_system.html:54
|
||||
msgid "View all versions"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_system.html:32
|
||||
#: snikket_web/templates/admin_system.html:55
|
||||
msgid "Average CPU use"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_system.html:40
|
||||
#: snikket_web/templates/admin_system.html:63
|
||||
msgid "Current memory use"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_system.html:50
|
||||
msgid "Snikket server status"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_system.html:71
|
||||
msgid "Storage used by shared files"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_system.html:79
|
||||
msgid "Connected devices"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_system.html:90
|
||||
msgid "Broadcast message"
|
||||
msgstr ""
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: snikket_web/templates/admin_users.html:19
|
||||
msgid "The user is an administrator."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_users.html:19
|
||||
msgid " (Administrator)"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_users.html:22
|
||||
msgid "The user is restricted."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_users.html:22
|
||||
msgid " (Restricted)"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/app.html:4
|
||||
@@ -796,8 +1079,8 @@ msgstr ""
|
||||
#: snikket_web/templates/invite_register.html:16
|
||||
#: snikket_web/templates/invite_reset_view.html:21
|
||||
#: snikket_web/templates/invite_view.html:41
|
||||
#: snikket_web/templates/invite_view.html:106
|
||||
#: snikket_web/templates/invite_view.html:134
|
||||
#: snikket_web/templates/invite_view.html:84
|
||||
#: snikket_web/templates/invite_view.html:112
|
||||
msgid "Open the app"
|
||||
msgstr ""
|
||||
|
||||
@@ -858,7 +1141,6 @@ msgid "You can now log in using your new password."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_reset_success.html:12
|
||||
#: snikket_web/templates/invite_success.html:18
|
||||
msgid "You can now safely close this page."
|
||||
msgstr ""
|
||||
|
||||
@@ -888,7 +1170,6 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_reset_view.html:26
|
||||
#: snikket_web/templates/invite_view.html:77
|
||||
msgid ""
|
||||
"Your camera will turn on. Point it at the square code below until it is "
|
||||
"within the highlighted square on your screen, and wait until the app "
|
||||
@@ -936,6 +1217,37 @@ msgid ""
|
||||
" password you chose during registration."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_success.html:18
|
||||
#, python-format
|
||||
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 ""
|
||||
|
||||
#: snikket_web/templates/invite_success.html:21
|
||||
msgid "Import successful"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_success.html:22
|
||||
msgid "Congratulations! Your account data has been successfully imported."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_success.html:26
|
||||
msgid "Moving to Snikket?"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_success.html:27
|
||||
msgid ""
|
||||
"If you are moving from a different Snikket instance or another XMPP-"
|
||||
"compatible service, you may optionally import the data (contacts, profile"
|
||||
" information, etc.) from your previous account. When you have exported "
|
||||
"the data from your previous account, upload it using the form below."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_success.html:30
|
||||
msgid "Upload account data"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:6
|
||||
#, python-format
|
||||
msgid "Invite to %(site_name)s | Snikket"
|
||||
@@ -976,7 +1288,7 @@ msgid "Get it on Google Play"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:30
|
||||
#: snikket_web/templates/invite_view.html:102
|
||||
#: snikket_web/templates/invite_view.html:80
|
||||
msgid "Download on the App Store"
|
||||
msgstr ""
|
||||
|
||||
@@ -1007,11 +1319,11 @@ msgid "Scan invite code"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:55
|
||||
#: snikket_web/templates/invite_view.html:84
|
||||
#: snikket_web/templates/invite_view.html:96
|
||||
#: snikket_web/templates/invite_view.html:112
|
||||
#: snikket_web/templates/invite_view.html:124
|
||||
#: snikket_web/templates/invite_view.html:140
|
||||
#: snikket_web/templates/invite_view.html:62
|
||||
#: snikket_web/templates/invite_view.html:74
|
||||
#: snikket_web/templates/invite_view.html:90
|
||||
#: snikket_web/templates/invite_view.html:102
|
||||
#: snikket_web/templates/invite_view.html:118
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
@@ -1022,59 +1334,39 @@ msgid ""
|
||||
"itself."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:63
|
||||
msgid "Using a QR code scanner"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:65
|
||||
msgid "Using the Snikket app"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:70
|
||||
msgid ""
|
||||
"Use a <em>QR code</em> scanner on your mobile device to scan the code "
|
||||
"below:"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:76
|
||||
msgid ""
|
||||
"Install the Snikket app on your mobile device, open it, and tap the "
|
||||
"'Scan' button at the top."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:93
|
||||
#: snikket_web/templates/invite_view.html:71
|
||||
msgid "Install on iOS"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:99
|
||||
#: snikket_web/templates/invite_view.html:77
|
||||
msgid ""
|
||||
"After downloading Snikket from the app store, you have to return to this "
|
||||
"After downloading Snikket from the App Store, you have to return to this "
|
||||
"invite link and tap on \"Open the app\" to proceed."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:101
|
||||
msgid "First download Snikket from the app store using the button below:"
|
||||
#: snikket_web/templates/invite_view.html:79
|
||||
msgid "First download Snikket from the App Store using the button below:"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:103
|
||||
#: snikket_web/templates/invite_view.html:131
|
||||
#: snikket_web/templates/invite_view.html:81
|
||||
#: snikket_web/templates/invite_view.html:109
|
||||
msgid ""
|
||||
"After the installation is complete, you can return to this page and tap "
|
||||
"the \"Open the app\" button to continue with the setup:"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:121
|
||||
#: snikket_web/templates/invite_view.html:130
|
||||
#: snikket_web/templates/invite_view.html:99
|
||||
#: snikket_web/templates/invite_view.html:108
|
||||
msgid "Install via F-Droid"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:127
|
||||
#: snikket_web/templates/invite_view.html:105
|
||||
msgid ""
|
||||
"After installing Snikket via F-Droid, you have to return to this invite "
|
||||
"link and tap on \"Open the app\" to proceed."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:129
|
||||
#: snikket_web/templates/invite_view.html:107
|
||||
msgid "First install Snikket from F-Droid using the button below:"
|
||||
msgstr ""
|
||||
|
||||
@@ -1086,10 +1378,6 @@ msgstr ""
|
||||
msgid "Invalid input"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/library.j2:108
|
||||
msgid "deleted"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/library.j2:122
|
||||
msgid "Can be used multiple times to create accounts on this Snikket service."
|
||||
msgstr ""
|
||||
@@ -1142,15 +1430,20 @@ msgstr ""
|
||||
msgid "Edit profile"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/user_home.html:38
|
||||
#: snikket_web/templates/user_home.html:33
|
||||
#: snikket_web/templates/user_manage_data.html:4
|
||||
msgid "Manage your data"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/user_home.html:39
|
||||
msgid "Your Snikket"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/user_home.html:40
|
||||
#: snikket_web/templates/user_home.html:41
|
||||
msgid "Manage users, invitations and circles of your Snikket service."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/user_home.html:42
|
||||
#: snikket_web/templates/user_home.html:43
|
||||
msgid "Admin panel"
|
||||
msgstr ""
|
||||
|
||||
@@ -1164,6 +1457,16 @@ msgid ""
|
||||
"any other connected devices."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/user_manage_data.html:8
|
||||
msgid "Export account"
|
||||
msgstr ""
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: snikket_web/templates/user_passwd.html:5
|
||||
msgid "Change your password"
|
||||
msgstr ""
|
||||
|
||||
BIN
snikket_web/translations/pl/LC_MESSAGES/messages.mo
Normal file
BIN
snikket_web/translations/pl/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
snikket_web/translations/zh_Hans_CN/LC_MESSAGES/messages.mo
Normal file
BIN
snikket_web/translations/zh_Hans_CN/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
1562
snikket_web/translations/zh_Hans_CN/LC_MESSAGES/messages.po
Normal file
1562
snikket_web/translations/zh_Hans_CN/LC_MESSAGES/messages.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,11 @@
|
||||
import asyncio
|
||||
import typing
|
||||
import urllib
|
||||
|
||||
import quart.flask_patch
|
||||
from quart import (
|
||||
Blueprint,
|
||||
Response,
|
||||
render_template,
|
||||
request,
|
||||
redirect,
|
||||
@@ -11,7 +13,7 @@ from quart import (
|
||||
flash,
|
||||
current_app,
|
||||
)
|
||||
import quart.exceptions
|
||||
import werkzeug.exceptions
|
||||
|
||||
import wtforms
|
||||
|
||||
@@ -57,7 +59,7 @@ _ACCESS_MODEL_CHOICES = [
|
||||
|
||||
|
||||
class ProfileForm(BaseForm):
|
||||
nickname = wtforms.TextField(
|
||||
nickname = wtforms.StringField(
|
||||
_l("Display name"),
|
||||
)
|
||||
|
||||
@@ -75,6 +77,16 @@ class ProfileForm(BaseForm):
|
||||
)
|
||||
|
||||
|
||||
class ImportAccountDataForm(BaseForm):
|
||||
account_data_file = wtforms.FileField(
|
||||
_l("Account data")
|
||||
)
|
||||
|
||||
action_upload = wtforms.SubmitField(
|
||||
_l("Upload"),
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/")
|
||||
@client.require_session()
|
||||
async def index() -> str:
|
||||
@@ -84,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:
|
||||
@@ -92,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."),
|
||||
@@ -116,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()
|
||||
@@ -138,7 +150,6 @@ async def profile() -> typing.Union[str, quart.Response]:
|
||||
mimetype = file_info.mimetype
|
||||
data = file_info.stream.read()
|
||||
if len(data) > max_avatar_size:
|
||||
print(len(data), max_avatar_size)
|
||||
form.avatar.errors.append(EAVATARTOOBIG)
|
||||
ok = False
|
||||
elif len(data) > 0:
|
||||
@@ -168,9 +179,49 @@ async def profile() -> typing.Union[str, quart.Response]:
|
||||
avatar_too_big_warning=EAVATARTOOBIG)
|
||||
|
||||
|
||||
class DataExportForm(BaseForm):
|
||||
action_export = wtforms.SubmitField(
|
||||
_l("Export")
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/manage_data", methods=["GET", "POST"])
|
||||
@client.require_session()
|
||||
async def manage_data() -> typing.Union[str, quart.Response]:
|
||||
form = DataExportForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
user_info = await client.get_user_info()
|
||||
# The UTF-8 version of the filename needs to be percent-encoded
|
||||
encoded_address = urllib.parse.quote(
|
||||
user_info["address"].encode(encoding='utf-8', errors='strict')
|
||||
)
|
||||
account_data = await client.export_account_data()
|
||||
if account_data is None:
|
||||
await flash(
|
||||
_("You currently have no account data to export."),
|
||||
"alert"
|
||||
)
|
||||
else:
|
||||
return Response(account_data,
|
||||
mimetype="application/xml",
|
||||
headers={
|
||||
# We provide the UTF-8 filename, but the ASCII
|
||||
# one will be used as a fallback for legacy
|
||||
# browsers (RFC 5987)
|
||||
"Content-Disposition": (
|
||||
'attachment; filename="account-data.xml"; '
|
||||
'filename*="UTF-8\'\'account-data-{}.xml"'
|
||||
).format(encoded_address)
|
||||
})
|
||||
return await render_template("user_manage_data.html",
|
||||
form=form,
|
||||
)
|
||||
|
||||
|
||||
@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()
|
||||
|
||||
@@ -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"
|
||||
@@ -207,7 +207,7 @@ def make_avatar_metadata_set_request(
|
||||
item,
|
||||
"metadata", xmlns=NS_USER_AVATAR_METADATA)
|
||||
|
||||
attr: typing.MutableMapping[str, str] = {
|
||||
attr: typing.Dict[str, str] = {
|
||||
"id": id_,
|
||||
"bytes": str(size),
|
||||
"type": mimetype,
|
||||
@@ -217,7 +217,12 @@ def make_avatar_metadata_set_request(
|
||||
if height is not None:
|
||||
attr["height"] = str(height)
|
||||
|
||||
ET.SubElement(metadata_wrap, "info", xmlns=NS_USER_AVATAR_METADATA, **attr)
|
||||
ET.SubElement(
|
||||
metadata_wrap,
|
||||
"info",
|
||||
xmlns=NS_USER_AVATAR_METADATA,
|
||||
**attr, # type: ignore
|
||||
)
|
||||
return req
|
||||
|
||||
|
||||
@@ -234,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:
|
||||
|
||||
@@ -5,13 +5,20 @@ action/delete:delete
|
||||
action/logout:logout
|
||||
action/login:login
|
||||
action/exit_to_app:exit_to_app
|
||||
action/lock:lock
|
||||
communication/import_export:import_export
|
||||
communication/qr_code:qrcode
|
||||
communication/vpn_key:passwd
|
||||
communication/rss_feed:broadcast
|
||||
content/add_circle_outline:add
|
||||
content/add_link:create_link
|
||||
content/remove_circle_outline:remove
|
||||
content/content_copy:copy
|
||||
content/link_off:remove_link
|
||||
content/send:send
|
||||
file/file_download:download
|
||||
file/file_upload:upload
|
||||
file/folder:folder
|
||||
navigation/arrow_back:back
|
||||
navigation/arrow_forward:forward
|
||||
navigation/cancel:cancel
|
||||
@@ -25,3 +32,4 @@ navigation/close:close
|
||||
image/edit:edit
|
||||
action/admin_panel_settings:admin
|
||||
content/link:link
|
||||
content/insights:insights
|
||||
|
||||
Reference in New Issue
Block a user