You've already forked snikket-web-portal
Compare commits
131 Commits
feature/ci
...
feature/sy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
a9a651be09 | ||
|
|
d2069289b0 | ||
|
|
552b5d2940 | ||
|
|
b0f9ae5d57 | ||
|
|
dd4a012612 | ||
|
|
e7aa0a2c45 | ||
|
|
ad229d6700 | ||
|
|
b822000f2e | ||
|
|
a6b67b3fdd | ||
|
|
885db355ab | ||
|
|
c3d5b06313 | ||
|
|
2dd8838852 | ||
|
|
5df2c3945a | ||
|
|
3eb8036ebd | ||
|
|
02ed390cd2 | ||
|
|
2506810b90 | ||
|
|
05d1b42dc4 | ||
|
|
5ef5b93eb9 | ||
|
|
0ff6e00e9d | ||
|
|
c04ac4bee0 | ||
|
|
3e19d42c2a | ||
|
|
03732ac06b | ||
|
|
c70228fed7 | ||
|
|
025172592f | ||
|
|
6de1e5313f | ||
|
|
3083c118a3 | ||
|
|
fa1b13fbdb | ||
|
|
ba30d728f4 | ||
|
|
af87301fa4 | ||
|
|
8ee0b0dd30 | ||
|
|
4a27ef9d72 | ||
|
|
9e9fdaf8d4 | ||
|
|
bdb186ca81 | ||
|
|
4ca9b82bce | ||
|
|
6dbe2c2d5e | ||
|
|
e410aedfef | ||
|
|
1713da61e7 | ||
|
|
53aac690df | ||
|
|
5e4009ca11 | ||
|
|
80860a3ac6 | ||
|
|
e9d479a78b | ||
|
|
aac56f49e9 | ||
|
|
52f0bee006 | ||
|
|
97c91b432d | ||
|
|
60647159f3 | ||
|
|
a21730f136 | ||
|
|
e35ab1b723 | ||
|
|
4de4509fc9 | ||
|
|
93e3b325b1 | ||
|
|
ceecfc861c | ||
|
|
2467e73781 | ||
|
|
2f34d39a09 | ||
|
|
de8589923b | ||
|
|
db3a1ac22f | ||
|
|
b48d130659 | ||
|
|
1aed573eb2 | ||
|
|
d4707196ec | ||
|
|
8a8d4c54bd | ||
|
|
ab534e3a59 | ||
|
|
4c128f1af2 | ||
|
|
8b551a8946 | ||
|
|
182d2301be | ||
|
|
6dba5e3a65 | ||
|
|
713da89445 | ||
|
|
9876e42fb7 | ||
|
|
8b66c5a063 | ||
|
|
ddf9f89d77 | ||
|
|
53e023f9ae | ||
|
|
e4d339627e | ||
|
|
cd3026911b | ||
|
|
d7da16f780 | ||
|
|
8ed0fbec25 | ||
|
|
5b812c773d | ||
|
|
fa61ee4e11 | ||
|
|
7402480c62 | ||
|
|
a68a469319 | ||
|
|
961f285fa5 | ||
|
|
7456295cb6 | ||
|
|
96f4b0d4f8 | ||
|
|
245434126e | ||
|
|
725dffc458 | ||
|
|
22783b837e | ||
|
|
ba18fe692f | ||
|
|
387a989caa | ||
|
|
ffab48cff0 | ||
|
|
17bf7cb140 | ||
|
|
408d837a0f | ||
|
|
56e1083ada | ||
|
|
2aa3d629da | ||
|
|
6779341db3 | ||
|
|
2db6cbe6fd | ||
|
|
9bc6e0b555 | ||
|
|
98a3eeba7c | ||
|
|
de97b08f01 | ||
|
|
f2dc970731 |
41
Dockerfile
41
Dockerfile
@@ -1,46 +1,31 @@
|
||||
FROM debian:buster
|
||||
FROM debian:buster-slim
|
||||
|
||||
ARG BUILD_SERIES=dev
|
||||
ARG BUILD_ID=0
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
COPY requirements.txt /opt/snikket-web-portal/requirements.txt
|
||||
COPY build-requirements.txt /opt/snikket-web-portal/build-requirements.txt
|
||||
COPY Makefile /opt/snikket-web-portal/Makefile
|
||||
COPY snikket_web/ /opt/snikket-web-portal/snikket_web
|
||||
COPY babel.cfg /opt/snikket-web-portal/babel.cfg
|
||||
|
||||
# This Dockerfile attempts to strike a balance between image size and time it
|
||||
# takes to do an incremental build on changes.
|
||||
# Improvements welcome.
|
||||
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 \
|
||||
; \
|
||||
apt-get clean ; rm -rf /var/lib/apt/lists
|
||||
|
||||
COPY requirements.txt /opt/snikket-web-portal/requirements.txt
|
||||
COPY build-requirements.txt /opt/snikket-web-portal/build-requirements.txt
|
||||
|
||||
WORKDIR /opt/snikket-web-portal
|
||||
|
||||
RUN set -eu; \
|
||||
pip3 install -r requirements.txt; \
|
||||
pip3 install -r build-requirements.txt; \
|
||||
rm -rf /root/.cache;
|
||||
|
||||
COPY Makefile /opt/snikket-web-portal/Makefile
|
||||
COPY snikket_web/ /opt/snikket-web-portal/snikket_web
|
||||
COPY babel.cfg /opt/snikket-web-portal/babel.cfg
|
||||
|
||||
# NOTE: abusing true(1) as a terrible way to disable a specific command. If
|
||||
# one merged all the RUN commands into one, one would want to run the
|
||||
# uninstall/remove commands there, but with the split up RUN commands it is
|
||||
# rather pointless.
|
||||
RUN set -eu; \
|
||||
make; \
|
||||
true pip3 uninstall -yr build-requirements.txt; \
|
||||
true apt-get remove -y build-essential make libpython3-dev; \
|
||||
true apt-get autoremove -y; \
|
||||
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
|
||||
@@ -50,5 +35,7 @@ ENV SNIKKET_WEB_PYENV=/etc/snikket-web-portal/env.py
|
||||
|
||||
ENV SNIKKET_WEB_PROSODY_ENDPOINT=http://127.0.0.1:5280/
|
||||
|
||||
HEALTHCHECK CMD nc -zv ${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_INTERFACE:-127.0.0.1} ${SNIKKET_TWEAK_PORTAL_INTERNAL_HTTP_PORT:-5765}
|
||||
|
||||
ADD docker/entrypoint.sh /entrypoint.sh
|
||||
ENTRYPOINT ["/bin/sh", "/entrypoint.sh"]
|
||||
|
||||
@@ -2,4 +2,7 @@
|
||||
|
||||
export SNIKKET_WEB_DOMAIN="$SNIKKET_DOMAIN"
|
||||
|
||||
exec hypercorn -b "127.0.0.1:5765" 'snikket_web:create_app()'
|
||||
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()'
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 421 KiB |
@@ -1,5 +1,5 @@
|
||||
aiohttp~=3.6
|
||||
quart~=0.11
|
||||
quart~=0.11,<0.15
|
||||
flask-wtf~=0.14
|
||||
hsluv~=0.0.2
|
||||
flask-babel~=1.0
|
||||
|
||||
@@ -48,6 +48,7 @@ async def proc() -> typing.Dict[str, typing.Any]:
|
||||
"text_to_css": colour.text_to_css,
|
||||
"lang": infra.selected_locale(),
|
||||
"user_info": user_info,
|
||||
"is_in_debug_mode": current_app.debug,
|
||||
}
|
||||
|
||||
|
||||
@@ -144,13 +145,22 @@ class AppConfig:
|
||||
site_name = environ.var("")
|
||||
avatar_cache_ttl = environ.var(1800, converter=int)
|
||||
languages = environ.var([
|
||||
"da",
|
||||
"de",
|
||||
"en",
|
||||
"fr",
|
||||
"id",
|
||||
"po",
|
||||
"it",
|
||||
"pl",
|
||||
"sv",
|
||||
], converter=autosplit)
|
||||
apple_store_url = environ.var("")
|
||||
# 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)))
|
||||
@@ -181,6 +191,8 @@ def create_app() -> quart.Quart:
|
||||
app.config["SITE_NAME"] = config.site_name or config.domain
|
||||
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(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
version_info = (0, 1, 0, "a0")
|
||||
version_info = (0, 2, 1, None)
|
||||
version = (
|
||||
".".join(map(str, version_info[:3])) +
|
||||
(f"-{version_info[3]}" if version_info[3] else "")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import json
|
||||
import resource
|
||||
import time
|
||||
import typing
|
||||
|
||||
from datetime import datetime
|
||||
@@ -18,13 +19,14 @@ from quart import (
|
||||
url_for,
|
||||
request,
|
||||
abort,
|
||||
flash,
|
||||
current_app,
|
||||
)
|
||||
import flask_wtf
|
||||
|
||||
from flask_babel import lazy_gettext as _l
|
||||
from flask_babel import lazy_gettext as _l, _
|
||||
|
||||
from . import prosodyclient
|
||||
from .infra import client, circle_name
|
||||
from . import prosodyclient, _version
|
||||
from .infra import client, circle_name, BaseForm
|
||||
|
||||
bp = Blueprint("admin", __name__, url_prefix="/admin")
|
||||
|
||||
@@ -32,11 +34,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(flask_wtf.FlaskForm): # type: ignore
|
||||
action_create = wtforms.StringField()
|
||||
class PasswordResetLinkPost(BaseForm):
|
||||
action_revoke = wtforms.StringField()
|
||||
|
||||
|
||||
@@ -47,15 +52,99 @@ async def users() -> str:
|
||||
await client.list_users(),
|
||||
key=lambda x: x.localpart
|
||||
)
|
||||
invite_form = InvitePost()
|
||||
await invite_form.init_choices()
|
||||
reset_form = PasswordResetLinkPost()
|
||||
return await render_template(
|
||||
"admin_users.html",
|
||||
users=users,
|
||||
reset_form=reset_form,
|
||||
invite_form=invite_form,
|
||||
)
|
||||
|
||||
|
||||
class DeleteUserForm(flask_wtf.FlaskForm): # type:ignore
|
||||
_LIMITED_ROLE_NAME = _("Limited")
|
||||
|
||||
|
||||
class EditUserForm(BaseForm):
|
||||
localpart = wtforms.StringField(
|
||||
_l("Login name"),
|
||||
)
|
||||
|
||||
display_name = wtforms.StringField(
|
||||
_l("Display name"),
|
||||
)
|
||||
|
||||
role = wtforms.RadioField(
|
||||
_l("Access Level"),
|
||||
choices=[
|
||||
# NOTE: enable this only after something has been done which
|
||||
# actually enforces the described restrictions :).
|
||||
# ("prosody:restricted", _LIMITED_ROLE_NAME),
|
||||
("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[quart.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")
|
||||
)
|
||||
@@ -69,6 +158,10 @@ async def delete_user(localpart: str) -> typing.Union[str, quart.Response]:
|
||||
if form.validate_on_submit():
|
||||
if form.action_delete.data:
|
||||
await client.delete_user_by_localpart(localpart)
|
||||
await flash(
|
||||
_("User deleted"),
|
||||
"success",
|
||||
)
|
||||
return redirect(url_for(".users"))
|
||||
|
||||
return await render_template(
|
||||
@@ -94,37 +187,47 @@ 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, quart.Response]:
|
||||
invite_info = await client.get_invite_by_id(
|
||||
id_,
|
||||
)
|
||||
if invite_info.jid is None:
|
||||
await flash(
|
||||
_("Password reset link not found"),
|
||||
"alert",
|
||||
)
|
||||
elif form.action_revoke.data:
|
||||
await client.delete_invite(form.action_revoke.data)
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
class InvitesListForm(flask_wtf.FlaskForm): # type:ignore
|
||||
class InvitesListForm(BaseForm):
|
||||
action_revoke = wtforms.StringField()
|
||||
|
||||
|
||||
class InvitePost(flask_wtf.FlaskForm): # type:ignore
|
||||
class InvitePost(BaseForm):
|
||||
circles = wtforms.SelectMultipleField(
|
||||
_l("Invite to circle"),
|
||||
# NOTE: This is for when/if we ever support multi-group invites.
|
||||
@@ -218,7 +321,7 @@ async def invitations() -> typing.Union[str, quart.Response]:
|
||||
)
|
||||
|
||||
|
||||
class InviteForm(flask_wtf.FlaskForm): # type:ignore
|
||||
class InviteForm(BaseForm):
|
||||
action_revoke = wtforms.SubmitField(
|
||||
_l("Revoke")
|
||||
)
|
||||
@@ -243,6 +346,10 @@ async def create_invite() -> typing.Union[str, quart.Response]:
|
||||
group_ids=form.circles.data,
|
||||
ttl=form.lifetime.data,
|
||||
)
|
||||
await flash(
|
||||
_("Invitation created"),
|
||||
"success",
|
||||
)
|
||||
return redirect(url_for(".edit_invite", id_=invite.id_))
|
||||
return await render_template("admin_create_invite.html",
|
||||
invite_form=form)
|
||||
@@ -255,7 +362,11 @@ async def edit_invite(id_: str) -> typing.Union[str, quart.Response]:
|
||||
invite_info = await client.get_invite_by_id(id_)
|
||||
except aiohttp.ClientResponseError as exc:
|
||||
if exc.status == 404:
|
||||
abort(404)
|
||||
await flash(
|
||||
_("No such invitation exists"),
|
||||
"alert",
|
||||
)
|
||||
return redirect(url_for(".invitations"))
|
||||
circles = await client.list_groups()
|
||||
circle_map = {
|
||||
circle.id_: circle
|
||||
@@ -266,6 +377,10 @@ async def edit_invite(id_: str) -> typing.Union[str, quart.Response]:
|
||||
if form.validate_on_submit():
|
||||
if form.action_revoke.data:
|
||||
await client.delete_invite(id_)
|
||||
await flash(
|
||||
_("Invitation revoked"),
|
||||
"success",
|
||||
)
|
||||
return redirect(url_for(".invitations"))
|
||||
return redirect(url_for(".edit_invite", id_=id_))
|
||||
|
||||
@@ -278,7 +393,7 @@ async def edit_invite(id_: str) -> typing.Union[str, quart.Response]:
|
||||
)
|
||||
|
||||
|
||||
class CirclePost(flask_wtf.FlaskForm): # type:ignore
|
||||
class CirclePost(BaseForm):
|
||||
name = wtforms.StringField(
|
||||
_l("Name"),
|
||||
validators=[wtforms.validators.InputRequired()],
|
||||
@@ -314,6 +429,10 @@ async def create_circle() -> typing.Union[str, quart.Response]:
|
||||
circle = await client.create_group(
|
||||
name=create_form.name.data,
|
||||
)
|
||||
await flash(
|
||||
_("Circle created"),
|
||||
"success",
|
||||
)
|
||||
return redirect(url_for(".edit_circle", id_=circle.id_))
|
||||
|
||||
return await render_template(
|
||||
@@ -322,7 +441,7 @@ async def create_circle() -> typing.Union[str, quart.Response]:
|
||||
)
|
||||
|
||||
|
||||
class EditCircleForm(flask_wtf.FlaskForm): # type:ignore
|
||||
class EditCircleForm(BaseForm):
|
||||
name = wtforms.StringField(
|
||||
_l("Name"),
|
||||
validators=[wtforms.validators.InputRequired()],
|
||||
@@ -359,28 +478,28 @@ async def edit_circle(id_: str) -> typing.Union[str, quart.Response]:
|
||||
)
|
||||
except aiohttp.ClientResponseError as exc:
|
||||
if exc.status == 404:
|
||||
await flash(
|
||||
_("No such circle exists"),
|
||||
"alert",
|
||||
)
|
||||
return redirect(url_for(".circles"))
|
||||
raise
|
||||
|
||||
circle_members = await asyncio.gather(*(
|
||||
client.get_user_by_localpart(
|
||||
localpart,
|
||||
session=session,
|
||||
)
|
||||
for localpart in sorted(circle.members)
|
||||
))
|
||||
|
||||
users = await client.list_users()
|
||||
users = sorted(
|
||||
await client.list_users(),
|
||||
key=lambda x: x.localpart
|
||||
)
|
||||
circle_members = [
|
||||
user for user in users
|
||||
if user.localpart in circle.members
|
||||
]
|
||||
|
||||
form = EditCircleForm()
|
||||
form.user_to_add.choices = sorted(
|
||||
(
|
||||
(u.localpart, u.localpart)
|
||||
for u in users
|
||||
if u.localpart not in circle.members
|
||||
),
|
||||
key=lambda x: x[1]
|
||||
)
|
||||
form.user_to_add.choices = [
|
||||
(user.localpart, user.localpart)
|
||||
for user in users
|
||||
if user.localpart not in circle.members
|
||||
]
|
||||
valid_users = [x[0] for x in form.user_to_add.choices]
|
||||
|
||||
invite_form = InvitePost()
|
||||
@@ -396,21 +515,36 @@ async def edit_circle(id_: str) -> typing.Union[str, quart.Response]:
|
||||
id_,
|
||||
new_name=form.name.data,
|
||||
)
|
||||
await flash(
|
||||
_("Circle data updated"),
|
||||
"success",
|
||||
)
|
||||
elif form.action_delete.data:
|
||||
await client.delete_group(id_)
|
||||
await flash(
|
||||
_("Circle deleted"),
|
||||
"success",
|
||||
)
|
||||
return redirect(url_for(".circles"))
|
||||
elif form.action_add_user.data:
|
||||
if form.user_to_add.data in valid_users:
|
||||
print("is valid")
|
||||
await client.add_group_member(
|
||||
id_,
|
||||
form.user_to_add.data,
|
||||
)
|
||||
await flash(
|
||||
_("User added to circle"),
|
||||
"success",
|
||||
)
|
||||
elif form.action_remove_user.data:
|
||||
await client.remove_group_member(
|
||||
id_,
|
||||
form.action_remove_user.data,
|
||||
)
|
||||
await flash(
|
||||
_("User removed from circle"),
|
||||
"success",
|
||||
)
|
||||
|
||||
return redirect(url_for(".edit_circle", id_=id_))
|
||||
else:
|
||||
@@ -423,3 +557,148 @@ 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, quart.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 quart.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
|
||||
|
||||
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
|
||||
|
||||
@@ -10,6 +11,7 @@ from quart import (
|
||||
)
|
||||
|
||||
import flask_babel
|
||||
import flask_wtf
|
||||
from flask_babel import _
|
||||
|
||||
from . import prosodyclient
|
||||
@@ -21,11 +23,20 @@ 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:
|
||||
selected = request.accept_languages.best_match(
|
||||
current_app.config['LANGUAGES']
|
||||
)
|
||||
) or current_app.config['LANGUAGES'][0]
|
||||
return selected
|
||||
|
||||
|
||||
@@ -41,12 +52,27 @@ def circle_name(c: typing.Any) -> str:
|
||||
return c.name
|
||||
|
||||
|
||||
def format_bytes(n: float) -> str:
|
||||
scale = math.floor(math.log(n, 1024))
|
||||
try:
|
||||
unit = BYTE_UNIT_SCALE_MAP[scale]
|
||||
factor = 1024**scale
|
||||
except ValueError:
|
||||
unit = "TiB"
|
||||
factor = 1024**4
|
||||
if factor > 1:
|
||||
return "{:.1f} {}".format(n / factor, unit)
|
||||
return "{} {}".format(n, unit)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@@ -55,3 +81,14 @@ def generate_error_id() -> str:
|
||||
return base64.b32encode(secrets.token_bytes(8)).decode(
|
||||
"ascii"
|
||||
).rstrip("=")
|
||||
|
||||
|
||||
class BaseForm(flask_wtf.FlaskForm): # type:ignore
|
||||
def __init__(self, *args: typing.Any, **kwargs: typing.Any):
|
||||
meta = kwargs["meta"] = dict(kwargs.get("meta", {}))
|
||||
if "locales" not in meta:
|
||||
locale = flask_babel.get_locale()
|
||||
if locale:
|
||||
meta["locales"] = [str(locale)]
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -16,10 +16,9 @@ from quart import (
|
||||
|
||||
import wtforms
|
||||
|
||||
import flask_wtf
|
||||
from flask_babel import lazy_gettext as _l
|
||||
|
||||
from .infra import client, selected_locale
|
||||
from .infra import client, selected_locale, BaseForm
|
||||
|
||||
|
||||
bp = Blueprint("invite", __name__)
|
||||
@@ -48,13 +47,20 @@ def context() -> typing.Mapping[str, typing.Any]:
|
||||
|
||||
|
||||
@bp.route("/<id_>")
|
||||
async def view(id_: str) -> str:
|
||||
async def view_old(id_: str) -> quart.Response:
|
||||
return redirect(url_for(".view", id_=id_))
|
||||
|
||||
|
||||
@bp.route("/<id_>/")
|
||||
async def view(id_: str) -> typing.Union[quart.Response,
|
||||
typing.Tuple[str, int],
|
||||
str]:
|
||||
try:
|
||||
invite = await client.get_public_invite_by_id(id_)
|
||||
except aiohttp.ClientResponseError as exc:
|
||||
if exc.status == 404:
|
||||
# invite expired
|
||||
return await render_template("invite_invalid.html")
|
||||
return await render_template("invite_invalid.html"), 404
|
||||
raise
|
||||
|
||||
if invite.reset_localpart is not None:
|
||||
@@ -79,16 +85,23 @@ async def view(id_: str) -> str:
|
||||
)
|
||||
apple_store_url = current_app.config["APPLE_STORE_URL"]
|
||||
|
||||
return await render_template(
|
||||
body = await render_template(
|
||||
"invite_view.html",
|
||||
invite=invite,
|
||||
play_store_url=play_store_url,
|
||||
apple_store_url=apple_store_url,
|
||||
f_droid_url="market://details?id=org.snikket.android",
|
||||
invite_id=id_,
|
||||
)
|
||||
return quart.Response(
|
||||
body,
|
||||
headers={
|
||||
"Link": "<{}> rel=\"alternate\"".format(invite.xmpp_uri),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class RegisterForm(flask_wtf.FlaskForm): # type:ignore
|
||||
class RegisterForm(BaseForm):
|
||||
localpart = wtforms.StringField(
|
||||
_l("Username"),
|
||||
)
|
||||
@@ -102,7 +115,7 @@ class RegisterForm(flask_wtf.FlaskForm): # type:ignore
|
||||
validators=[wtforms.validators.InputRequired(),
|
||||
wtforms.validators.EqualTo(
|
||||
"password",
|
||||
_l("The passwords must match")
|
||||
_l("The passwords must match.")
|
||||
)]
|
||||
)
|
||||
|
||||
@@ -134,15 +147,15 @@ async def register(id_: str) -> typing.Union[str, quart.Response]:
|
||||
except aiohttp.ClientResponseError as exc:
|
||||
if exc.status == 409:
|
||||
form.localpart.errors.append(
|
||||
_l("That username is already taken")
|
||||
_l("That username is already taken.")
|
||||
)
|
||||
elif exc.status == 403:
|
||||
form.localpart.errors.append(
|
||||
_l("Registration was declined for unknown reasons")
|
||||
_l("Registration was declined for unknown reasons.")
|
||||
)
|
||||
elif exc.status == 400:
|
||||
form.localpart.errors.append(
|
||||
_l("The username is not valid")
|
||||
_l("The username is not valid.")
|
||||
)
|
||||
elif exc.status == 404:
|
||||
return redirect(url_for(".view", id_=id_))
|
||||
@@ -159,7 +172,7 @@ async def register(id_: str) -> typing.Union[str, quart.Response]:
|
||||
)
|
||||
|
||||
|
||||
class ResetForm(flask_wtf.FlaskForm): # type:ignore
|
||||
class ResetForm(BaseForm):
|
||||
password = wtforms.PasswordField(
|
||||
_l("Password"),
|
||||
)
|
||||
@@ -169,7 +182,7 @@ class ResetForm(flask_wtf.FlaskForm): # type:ignore
|
||||
validators=[wtforms.validators.InputRequired(),
|
||||
wtforms.validators.EqualTo(
|
||||
"password",
|
||||
_l("The passwords must match")
|
||||
_l("The passwords must match.")
|
||||
)]
|
||||
)
|
||||
|
||||
@@ -202,7 +215,7 @@ async def reset(id_: str) -> typing.Union[str, quart.Response]:
|
||||
except aiohttp.ClientResponseError as exc:
|
||||
if exc.status == 403:
|
||||
form.localpart.errors.append(
|
||||
_l("Registration was declined for unknown reasons")
|
||||
_l("Registration was declined for unknown reasons.")
|
||||
)
|
||||
elif exc.status == 404:
|
||||
return redirect(url_for(".view", id_=id_))
|
||||
|
||||
@@ -15,23 +15,23 @@ from quart import (
|
||||
render_template,
|
||||
request,
|
||||
Response,
|
||||
flash,
|
||||
)
|
||||
|
||||
import babel
|
||||
import wtforms
|
||||
|
||||
import flask_wtf
|
||||
|
||||
from flask_babel import lazy_gettext as _l, _
|
||||
|
||||
from . import xmpputil, _version
|
||||
from .infra import client
|
||||
from .infra import client, BaseForm
|
||||
|
||||
|
||||
bp = quart.Blueprint("main", __name__)
|
||||
|
||||
|
||||
class LoginForm(flask_wtf.FlaskForm): # type:ignore
|
||||
class LoginForm(BaseForm):
|
||||
address = wtforms.TextField(
|
||||
_l("Address"),
|
||||
validators=[wtforms.validators.InputRequired()],
|
||||
@@ -52,6 +52,9 @@ async def index() -> quart.Response:
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
ERR_CREDENTIALS_INVALID = _l("Invalid username or password.")
|
||||
|
||||
|
||||
@bp.route("/login", methods=["GET", "POST"])
|
||||
async def login() -> typing.Union[str, quart.Response]:
|
||||
if client.has_session and (await client.test_session()):
|
||||
@@ -63,33 +66,48 @@ async def login() -> typing.Union[str, quart.Response]:
|
||||
localpart, domain, resource = xmpputil.split_jid(jid)
|
||||
if not localpart:
|
||||
localpart, domain = domain, current_app.config["SNIKKET_DOMAIN"]
|
||||
jid = "{}@{}".format(localpart, domain)
|
||||
password = form.password.data
|
||||
try:
|
||||
await client.login(jid, password)
|
||||
except quart.exceptions.Unauthorized:
|
||||
form.password.errors.append(
|
||||
_("Invalid username or password.")
|
||||
)
|
||||
if domain != current_app.config["SNIKKET_DOMAIN"]:
|
||||
# (a) prosody throws a 400 at us and I prefer to catch that here
|
||||
# and (b) I don’t want to pass on this obviously not-for-here
|
||||
# password further than necessary.
|
||||
form.password.errors.append(ERR_CREDENTIALS_INVALID)
|
||||
else:
|
||||
return redirect(url_for('user.index'))
|
||||
jid = "{}@{}".format(localpart, domain)
|
||||
password = form.password.data
|
||||
try:
|
||||
await client.login(jid, password)
|
||||
except quart.exceptions.Unauthorized:
|
||||
form.password.errors.append(ERR_CREDENTIALS_INVALID)
|
||||
else:
|
||||
await flash(
|
||||
_("Login successful!"),
|
||||
"success"
|
||||
)
|
||||
return redirect(url_for('user.index'))
|
||||
|
||||
return await render_template("login.html", form=form)
|
||||
|
||||
|
||||
@bp.route("/meta/about.html")
|
||||
async def about() -> str:
|
||||
version = None
|
||||
extra_versions = {}
|
||||
if current_app.debug:
|
||||
|
||||
if current_app.debug or client.is_admin_session:
|
||||
version = _version.version
|
||||
extra_versions["Quart"] = quart.__version__
|
||||
extra_versions["aiohttp"] = aiohttp.__version__
|
||||
extra_versions["babel"] = babel.__version__
|
||||
extra_versions["wtforms"] = wtforms.__version__
|
||||
extra_versions["flask-wtf"] = flask_wtf.__version__
|
||||
try:
|
||||
extra_versions["Prosody"] = await client.get_server_version()
|
||||
except quart.exceptions.Unauthorized:
|
||||
extra_versions["Prosody"] = "unknown"
|
||||
|
||||
return await render_template(
|
||||
"about.html",
|
||||
version=_version.version,
|
||||
version=version,
|
||||
extra_versions=extra_versions,
|
||||
)
|
||||
|
||||
@@ -105,6 +123,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:
|
||||
@@ -144,3 +163,8 @@ async def avatar(from_: str, code: str) -> quart.Response:
|
||||
|
||||
response.set_data(data)
|
||||
return response
|
||||
|
||||
|
||||
@bp.route("/_health")
|
||||
async def health() -> Response:
|
||||
return Response("STATUS OK", content_type="text/plain")
|
||||
|
||||
@@ -44,6 +44,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 +64,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"),
|
||||
)
|
||||
|
||||
|
||||
@@ -332,15 +342,18 @@ class ProsodyClient:
|
||||
)
|
||||
)
|
||||
|
||||
def _store_token_in_session(self, token_info: TokenInfo) -> None:
|
||||
http_session[self.SESSION_TOKEN] = token_info.token
|
||||
http_session[self.SESSION_CACHED_SCOPE] = " ".join(token_info.scopes)
|
||||
|
||||
async def login(self, jid: str, password: str) -> bool:
|
||||
async with self._plain_session as session:
|
||||
token_info = await self._oauth2_bearer_token(
|
||||
session, jid, password,
|
||||
)
|
||||
|
||||
http_session[self.SESSION_TOKEN] = token_info.token
|
||||
self._store_token_in_session(token_info)
|
||||
http_session[self.SESSION_ADDRESS] = jid
|
||||
http_session[self.SESSION_CACHED_SCOPE] = " ".join(token_info.scopes)
|
||||
return True
|
||||
|
||||
@property
|
||||
@@ -445,6 +458,13 @@ class ProsodyClient:
|
||||
headers=final_headers,
|
||||
data=serialised) as resp:
|
||||
if resp.status != 200:
|
||||
self.logger.debug(
|
||||
"IQ HTTP response (in-reply-to id=%s) with non-OK status "
|
||||
"%s: %s",
|
||||
id_,
|
||||
resp.status,
|
||||
resp.reason,
|
||||
)
|
||||
abort(resp.status)
|
||||
reply_payload = await resp.read()
|
||||
self.logger.debug(
|
||||
@@ -490,9 +510,32 @@ 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
|
||||
async def get_server_version(self, session: aiohttp.ClientSession) -> str:
|
||||
_, domain, _ = split_jid(self.session_address)
|
||||
req = {
|
||||
"kind": "iq",
|
||||
"type": "get",
|
||||
"version": {},
|
||||
"to": domain,
|
||||
}
|
||||
|
||||
async with session.post(self._rest_endpoint, json=req) as resp:
|
||||
if resp.status != 200:
|
||||
return "unknwn"
|
||||
try:
|
||||
return (await resp.json())["version"]["version"]
|
||||
except Exception as exc:
|
||||
self.logger.debug(
|
||||
"failed to parse prosody version from response"
|
||||
" (%s: %s)",
|
||||
type(exc), exc,
|
||||
)
|
||||
return "unknown"
|
||||
|
||||
@autosession
|
||||
async def get_user_nickname(
|
||||
self,
|
||||
@@ -767,7 +810,7 @@ class ProsodyClient:
|
||||
# got there, replacing the current session token on the way.
|
||||
|
||||
async with self._plain_session as session:
|
||||
token = await self._oauth2_bearer_token(
|
||||
token_info = await self._oauth2_bearer_token(
|
||||
session,
|
||||
self.session_address,
|
||||
current_password,
|
||||
@@ -779,14 +822,14 @@ class ProsodyClient:
|
||||
new_password
|
||||
),
|
||||
headers={
|
||||
"Authorization": "Bearer {}".format(token),
|
||||
"Authorization": "Bearer {}".format(token_info.token),
|
||||
},
|
||||
sensitive=True,
|
||||
)
|
||||
# TODO: error handling
|
||||
# TODO: obtain a new token using the new password to allow the
|
||||
# server to expire/revoke all tokens on password change.
|
||||
http_session[self.SESSION_TOKEN] = token
|
||||
self._store_token_in_session(token_info)
|
||||
|
||||
def _raise_error_from_response(
|
||||
self,
|
||||
@@ -825,6 +868,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,
|
||||
@@ -1109,3 +1175,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()
|
||||
|
||||
@@ -252,3 +252,4 @@ $h-sizes: [200.0%, 174.11011266%, 151.57165665%, 131.95079108%, 114.8698355%, 10
|
||||
$h-small-sizes: [150.0%, 138.31618672%, 127.54245006%, 117.60790225%, 108.44717712%, 100.0%];
|
||||
$small-screen-threshold: 40rem;
|
||||
$medium-screen-threshold: 60rem;
|
||||
$large-screen-threshold: 80rem;
|
||||
|
||||
@@ -33,13 +33,35 @@ body {
|
||||
|
||||
main {
|
||||
padding: $w-l1;
|
||||
margin-left: auto;
|
||||
max-width: 60rem;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
#mwrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
> .filler, > .flashbox {
|
||||
flex: 1 1 1rem;
|
||||
}
|
||||
|
||||
> main {
|
||||
flex: 0 1 60rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $large-screen-threshold) {
|
||||
#mwrap {
|
||||
display: block;
|
||||
|
||||
> main {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flashbox > div.box > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* top bar */
|
||||
@@ -67,6 +89,10 @@ div#topbar {
|
||||
font-size: $_top-h-size;
|
||||
line-height: 1.5;
|
||||
|
||||
body.debug & {
|
||||
color: red;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-screen-threshold) {
|
||||
font-size: $_top-h-small-size;
|
||||
}
|
||||
@@ -134,22 +160,20 @@ body > footer {
|
||||
background-color: $gray-100;
|
||||
color: $gray-800;
|
||||
padding: 0 $w-l1;
|
||||
font-size: 92.21079115%;
|
||||
|
||||
ul {
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
text-align: center;
|
||||
line-height: 1.6267076567643135;
|
||||
}
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: $w-l1 0;
|
||||
}
|
||||
|
||||
li:before {
|
||||
content: '•';
|
||||
padding-right: $w-s2;
|
||||
display: block;
|
||||
margin: $w-s1 0;
|
||||
}
|
||||
|
||||
a, a:visited, a:hover, a:active, a:focus {
|
||||
@@ -330,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;
|
||||
@@ -993,6 +1026,23 @@ div.profile-card {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
input[type="submit"], button, .button {
|
||||
&.slimmify {
|
||||
> svg.icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
> span {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
top: -100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* clipboard button */
|
||||
|
||||
@@ -54,6 +54,8 @@ div.install-buttons {
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
list-style-type: none;
|
||||
margin: $w-l1 0;
|
||||
padding: 0;
|
||||
@@ -74,6 +76,10 @@ img.play {
|
||||
height: $w-l3;
|
||||
}
|
||||
|
||||
img.fdroid {
|
||||
height: $w-l3;
|
||||
}
|
||||
|
||||
.tabbox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
BIN
snikket_web/static/img/f-droid-badge.png
Normal file
BIN
snikket_web/static/img/f-droid-badge.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -37,6 +37,11 @@ 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/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 +52,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 +83,11 @@ 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: navigation/arrow_back/materialiconsround/24px.svg -->
|
||||
<symbol id="icon-back" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
@@ -137,4 +153,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: 16 KiB |
@@ -3,5 +3,7 @@
|
||||
{#- -#}
|
||||
<li>{% trans about_url=url_for('main.about') %}A <a href="{{ about_url }}">Snikket</a> service{% endtrans %}</li>
|
||||
{#- -#}
|
||||
<li>{% trans %}“Snikket” and the parrot logo are trademarks of Snikket Community Interest Company.{% endtrans %}</li>
|
||||
{#- -#}
|
||||
</ul>
|
||||
</footer>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "library.j2" import standard_button %}
|
||||
{% block head_lead %}
|
||||
<title>About Snikket</title>
|
||||
<title>{% trans %}About Snikket{% endtrans %}</title>
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<main>
|
||||
@@ -14,10 +14,12 @@
|
||||
<p>{% trans agpl_url="https://www.gnu.org/licenses/agpl.html" %}The web portal software is licensed under the terms of the <a href="{{ agpl_url }}">Affero GNU General Public License, version 3.0 or later</a>. The full terms of the license can be reviewed using the aforementioned link.{% endtrans %}</p>
|
||||
<p>{% trans source_url="https://github.com/snikket-im/snikket-web-portal/" %}The source code of the web portal can be downloaded and viewed in <a href="{{ source_url }}">its GitHub repository</a>.{% endtrans %}</p>
|
||||
<p>{% trans source_url="https://material.io/resources/icons/", apache20_url="https://www.apache.org/licenses/LICENSE-2.0.txt" %}The icons used in the web portal are <a href="{{ source_url }}">Google’s Material Icons</a>, made available by Google under the terms of the <a href="{{ apache20_url }}">Apache 2.0 License</a>.{% endtrans %}</p>
|
||||
<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 ({{ version }})
|
||||
Snikket Web Portal{% if version %} ({{ version }}){% 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<col/>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans %}Valid until{% endtrans %}</th>
|
||||
<th>{% trans %}Expires{% endtrans %}</th>
|
||||
<th class="collapsible">{% trans %}Type{% endtrans %}</th>
|
||||
<th class="collapsible">{% trans %}Circle{% endtrans %}</th>
|
||||
<th>{% trans %}Actions{% endtrans %}</th>
|
||||
|
||||
@@ -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>
|
||||
|
||||
97
snikket_web/templates/admin_system.html
Normal file
97
snikket_web/templates/admin_system.html
Normal file
@@ -0,0 +1,97 @@
|
||||
{% 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 %}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,5 +33,5 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table></div>
|
||||
</form>
|
||||
{%- include "admin_create_invite_form.html" -%}
|
||||
{% endblock %}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
{% endblock %}
|
||||
{% block topbar_right %}
|
||||
{{- super() -}}
|
||||
{% call standard_button("logout", url_for("user.logout"), class="tertiary") %}{% trans %}Log out{% endtrans %}{% endcall %}
|
||||
{% call standard_button("logout", url_for("user.logout"), class="tertiary slimmify") %}{% trans %}Log out{% endtrans %}{% endcall %}
|
||||
{%- endblock %}
|
||||
|
||||
@@ -16,5 +16,5 @@
|
||||
<meta name="msapplication-TileColor" content="#fbd308">
|
||||
<meta name="theme-color" content="#fbd308">
|
||||
</head>
|
||||
<body{% if body_id | default(False) %} id="{{ body_id }}"{% endif %}{% if body_class | default(False) %} class="{{ body_class }}"{% endif %}{% if onload | default(False) %} onload="{{ onload }}"{% endif %}>{% block body %}{% endblock %}</body>
|
||||
<body{% if body_id | default(False) %} id="{{ body_id }}"{% endif %} class="{% if is_in_debug_mode %}debug{% endif %}{% if body_class | default(False) %} {{ body_class }}{% endif %}"{% if onload | default(False) %} onload="{{ onload }}"{% endif %}>{% block body %}{% endblock %}</body>
|
||||
</html>
|
||||
|
||||
@@ -5,6 +5,6 @@
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for("static", filename="css/invite.css") }}">
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<div id="mwrap"><main>{% block content %}{% endblock %}</main></div>
|
||||
<div id="mwrap"><div class="filler"></div><main>{% block content %}{% endblock %}</main><div class="filler"></div></div>
|
||||
{%- include "_footer.html" -%}
|
||||
{% endblock %}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<title>{% trans site_name=config["SITE_NAME"] %}Invite to {{ site_name }} | Snikket{% endtrans %}</title>
|
||||
<script async type="text/javascript" src="{{ url_for("static", filename="js/invite-magic.js") }}"></script>
|
||||
<script async type="text/javascript" src="{{ url_for("static", filename="js/qrcode.min.js") }}"></script>
|
||||
<link rel="alternate" href="{{ invite.xmpp_uri }}">
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="elevated box el-3">
|
||||
@@ -26,11 +27,12 @@
|
||||
<ul>
|
||||
<li><a href="{{ play_store_url }}"><img alt='{% trans %}Get it on Google Play{% endtrans %}' src='https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png' class="play"/></a></li>
|
||||
{%- if apple_store_url -%}
|
||||
<li><a href="{{ apple_store_url }}"><img alt='{% trans %}Download on the App Store{% endtrans %}' src="{{ apple_store_badge() }}" class="apple"></a></li>
|
||||
<li><a href="{{ apple_store_url }}" class="popover" data-popover-id="apple-popover"><img alt='{% trans %}Download on the App Store{% endtrans %}' src="{{ apple_store_badge() }}" class="apple"></a></li>
|
||||
{%- endif -%}
|
||||
<li><a href="{{ f_droid_url }}" class="popover" data-popover-id="fdroid-popover"><img alt='{% trans %}Get it on F-Droid{% endtrans %}' src='{{ url_for('static', filename='img/f-droid-badge.png') }}' class="fdroid"/></a></li>
|
||||
</ul>
|
||||
{%- call standard_button("qrcode", "#qr-modal", class="primary", onclick="open_modal(this); return false;") -%}
|
||||
{% trans %}Not on mobile?{% endtrans %}
|
||||
{% trans %}Send to mobile device{% endtrans %}
|
||||
{%- endcall -%}
|
||||
</div>
|
||||
<p>{% trans %}After installation the app should automatically open and prompt you to create an account. If not, simply click the button below.{% endtrans %}</p>
|
||||
@@ -83,10 +85,77 @@
|
||||
{%- endcall -%}
|
||||
</div>
|
||||
</div>
|
||||
{%- if apple_store_url -%}
|
||||
<div id="apple-popover" class="modal" tabindex="-1" role="dialog" aria-hidden="true" style="display: none;" onclick="close_modal(this); return false;">
|
||||
<div role="document" class="elevated box el-2" onclick="event.stopPropagation();">
|
||||
<header class="modal-title">
|
||||
{#- -#}
|
||||
<span>{% trans %}Install on iOS{% endtrans %}</span>
|
||||
{#- -#}
|
||||
{%- call action_button("close", "#", onclick="close_modal(this.parentNode.parentNode.parentNode); return false;", class="tertiary") -%}
|
||||
{% 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>
|
||||
<ol>
|
||||
<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>
|
||||
{%- call standard_button("exit_to_app", invite.xmpp_uri, class="primary") -%}
|
||||
{% trans %}Open the app{% endtrans %}
|
||||
{%- endcall -%}
|
||||
</p></li>
|
||||
</ol>
|
||||
{#- -#}
|
||||
{%- call standard_button("close", "#", onclick="close_modal(this.parentNode.parentNode); return false;", class="secondary") -%}
|
||||
{% trans %}Close{% endtrans %}
|
||||
{%- endcall -%}
|
||||
</div>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
<div id="fdroid-popover" class="modal" tabindex="-1" role="dialog" aria-hidden="true" style="display: none;" onclick="close_modal(this); return false;">
|
||||
<div role="document" class="elevated box el-2" onclick="event.stopPropagation();">
|
||||
<header class="modal-title">
|
||||
{#- -#}
|
||||
<span>{% trans %}Install via F-Droid{% endtrans %}</span>
|
||||
{#- -#}
|
||||
{%- call action_button("close", "#", onclick="close_modal(this.parentNode.parentNode.parentNode); return false;", class="tertiary") -%}
|
||||
{% trans %}Close{% endtrans %}
|
||||
{%- endcall -%}
|
||||
</header>
|
||||
<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>
|
||||
<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") -%}
|
||||
{% trans %}Open the app{% endtrans %}
|
||||
{%- endcall -%}
|
||||
</p></li>
|
||||
</ol>
|
||||
{#- -#}
|
||||
{%- call standard_button("close", "#", onclick="close_modal(this.parentNode.parentNode); return false;", class="secondary") -%}
|
||||
{% trans %}Close{% endtrans %}
|
||||
{%- endcall -%}
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
var catch_popover = function() {
|
||||
open_modal(this);
|
||||
return false;
|
||||
}
|
||||
|
||||
var onload = function() {
|
||||
apply_qr_code(document.getElementById("qr-invite-page"));
|
||||
apply_qr_code(document.getElementById("qr-uri"));
|
||||
var popover_as = document.getElementsByClassName("popover");
|
||||
for (var i = 0; i < popover_as.length; ++i) {
|
||||
var a = popover_as[i];
|
||||
a.onclick = catch_popover;
|
||||
a.href = "#" + a.dataset.popoverId;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
<div class="box warning">{#- -#}
|
||||
<header>{% trans %}Invalid input{% endtrans %}</header>
|
||||
{%- if error_list | length == 1 -%}
|
||||
<p>{{ error_list[0] }}.</p>
|
||||
<p>{{ error_list[0] }}</p>
|
||||
{%- else -%}
|
||||
<ul>
|
||||
{%- for error in error_list -%}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "library.j2" import box, form_button %}
|
||||
{% from "library.j2" import box, form_button, render_errors %}
|
||||
{% set body_id = "login" %}
|
||||
{% block head_lead %}
|
||||
<title>{{ _("Snikket Login") }}</title>
|
||||
@@ -9,16 +9,16 @@
|
||||
{{ super() }}
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<div id="mwrap"><main><div class="form layout-expanded">
|
||||
<div id="mwrap"><div class="filler"></div><main><div class="form layout-expanded">
|
||||
<h1 class="form-title">{{ config["SITE_NAME"] }}</h1>
|
||||
<p class="form-desc">{{ _("Enter your Snikket address and password to manage your account.") }}</p>
|
||||
<form method="POST" action="{{ url_for('.login') }}" name="login">
|
||||
<form method="POST" action="{{ url_for('.login') }}" name="login" id="login-form" onsubmit="return domainCheck();" data-addressid="{{ form.address.id }}" data-domain="{{ config["SNIKKET_DOMAIN"] }}">
|
||||
{{ form.csrf_token }}
|
||||
{% if form.errors %}
|
||||
{% call box("alert", _("Login failed")) %}
|
||||
<p>{{ form.errors.values() | flatten | join(", ")}}</p>
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
{% call render_errors(form) %}{% endcall %}
|
||||
<div class="box alert" role="alert" style="display: none;" id="id-warning">
|
||||
<header>{% trans %}Incorrect address{% endtrans %}</header>
|
||||
<p>{% trans snikket_domain=config["SNIKKET_DOMAIN"] %}This Snikket service only hosts addresses ending in <em>@{{ snikket_domain }}</em>. Your password was not sent.{% endtrans %}</p>
|
||||
</div>
|
||||
<div class="f-ebox">
|
||||
{{ form.address.label(class="a11y-only") }}
|
||||
{{ form.address(placeholder=form.address.label.text) }}
|
||||
@@ -31,8 +31,22 @@
|
||||
{%- call form_button("login", form.action_signin, class="primary") -%}{% endcall -%}
|
||||
</div>
|
||||
</from>
|
||||
</div></main></div>
|
||||
<footer>
|
||||
<ul><li>{% trans about_url=url_for('.about') %}A <a href="{{ about_url }}">Snikket</a> service{% endtrans %}</li></ul>
|
||||
</footer>
|
||||
<script type="text/javascript">
|
||||
var domainCheck = function() {
|
||||
var form = document.getElementById("login-form");
|
||||
var addressId = form.dataset.addressid;
|
||||
var addressField = document.getElementById(addressId);
|
||||
var domain = form.dataset.domain;
|
||||
var address = addressField.value;
|
||||
var errorBox = document.getElementById("id-warning");
|
||||
if (address.includes("@") && !address.endsWith(domain)) {
|
||||
errorBox.style.display = "block";
|
||||
return false;
|
||||
}
|
||||
errorBox.style.display = "none";
|
||||
return true;
|
||||
};
|
||||
</script>
|
||||
</div></main><div class="filler"></div></div>
|
||||
{%- include "_footer.html" -%}
|
||||
{% endblock %}
|
||||
|
||||
@@ -7,6 +7,25 @@
|
||||
<div class="filler"></div>
|
||||
{% block topbar_right %}{% endblock %}
|
||||
</div>
|
||||
<div id="mwrap"><main>{% block content %}{% endblock %}</main></div>
|
||||
<div id="mwrap">
|
||||
{#- -#}
|
||||
<div class="flashbox" id="flashbox">
|
||||
{%- for category, message in get_flashed_messages(True) -%}
|
||||
<div class="box {{ category }} el-5" role="alert">
|
||||
{% if category == "success" %}
|
||||
<header>{% trans %}Operation successful{% endtrans %}</header>
|
||||
{% elif category == "alert" %}
|
||||
<header>{% trans %}Error{% endtrans %}</header>
|
||||
{% endif %}
|
||||
<p>{{ message }}</p>
|
||||
</div>
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
{#- -#}
|
||||
<main>{% block content %}{% endblock %}</main>
|
||||
{#- -#}
|
||||
<div class="filler"></div>
|
||||
{#- -#}
|
||||
</div>
|
||||
{%- include "_footer.html" -%}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
{% extends "app.html" %}
|
||||
{% from "library.j2" import standard_button, form_button %}
|
||||
{% block head_lead %}
|
||||
<title>Snikket Web Portal</title>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="form layout-expanded"><form method="POST">
|
||||
<h2 class="form-title">{% trans %}Sign out of the Snikket Web Portal{% endtrans %}</h2>
|
||||
<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 -%}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
{% extends "app.html" %}
|
||||
{% from "library.j2" import standard_button, custom_form_button, render_errors %}
|
||||
{% block head_lead %}
|
||||
<title>Snikket Web Portal</title>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="form layout-expanded"><form method="POST">
|
||||
<h1 class="form-title">{% trans %}Change your password{% endtrans %}</h1>
|
||||
@@ -27,7 +24,7 @@
|
||||
<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 -%}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
{% extends "app.html" %}
|
||||
{% from "library.j2" import standard_button, form_button, avatar with context %}
|
||||
{% block head_lead %}
|
||||
<title>Snikket Web Portal</title>
|
||||
{% endblock %}
|
||||
{% from "library.j2" import standard_button, form_button, render_errors, avatar with context %}
|
||||
{% block content %}
|
||||
<h1>{% trans %}Update your profile{% endtrans %}</h1>
|
||||
<div class="form layout-expanded"><form method="POST" enctype="multipart/form-data">
|
||||
<h2 class="form-title">{% trans %}Profile{% endtrans %}</h2>
|
||||
{{ form.csrf_token }}
|
||||
{% call render_errors(form) %}{% endcall %}
|
||||
<div class="f-ebox">
|
||||
{{ form.nickname.label }}
|
||||
{{ form.nickname(placeholder=user_info.username) }}
|
||||
@@ -16,7 +14,10 @@
|
||||
{{ form.avatar.label }}
|
||||
<div class="avatar-wrap">
|
||||
{%- call avatar(user_info.address, user_info.avatar_hash ) %}{% endcall -%}
|
||||
{{ form.avatar }}
|
||||
{{ form.avatar(accept="image/png",
|
||||
data_maxsize=max_avatar_size,
|
||||
data_warning_header=avatar_too_big_warning_header,
|
||||
data_maxsize_warning=avatar_too_big_warning) }}
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="form-title">{% trans %}Visibility{% endtrans %}</h3>
|
||||
@@ -28,8 +29,27 @@
|
||||
</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">
|
||||
document.getElementById("{{ form.avatar.id }}").onchange = function() {
|
||||
var maxsize_s = this.dataset.maxsize;
|
||||
var maxsize = parseInt(maxsize_s);
|
||||
var existing_alert = document.getElementById("avatar-alert");
|
||||
if (existing_alert) {
|
||||
existing_alert.parentNode.removeChild(existing_alert);
|
||||
}
|
||||
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>
|
||||
{% endblock %}
|
||||
|
||||
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.
1432
snikket_web/translations/da/LC_MESSAGES/messages.po
Normal file
1432
snikket_web/translations/da/LC_MESSAGES/messages.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
snikket_web/translations/de/LC_MESSAGES/messages.mo
Normal file
BIN
snikket_web/translations/de/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: SnikketWeb 0.1.0\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2021-01-30 12:45+0100\n"
|
||||
"PO-Revision-Date: 2021-01-31 12:54+0000\n"
|
||||
"POT-Creation-Date: 2021-03-25 17:32+0100\n"
|
||||
"PO-Revision-Date: 2021-05-18 13:28+0000\n"
|
||||
"Last-Translator: Jonas Schäfer <jonas@zombofant.net>\n"
|
||||
"Language-Team: German <https://i18n.sotecware.net/projects/snikket/"
|
||||
"web-portal/de/>\n"
|
||||
@@ -17,127 +17,217 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 4.4.2\n"
|
||||
"X-Generator: Weblate 4.5.1\n"
|
||||
"Generated-By: Babel 2.9.0\n"
|
||||
|
||||
#: snikket_web/admin.py:60
|
||||
#: snikket_web/admin.py:59
|
||||
msgid "Limited"
|
||||
msgstr "Eingeschränkt"
|
||||
|
||||
#: snikket_web/admin.py:64 snikket_web/templates/admin_delete_user.html:10
|
||||
#: snikket_web/templates/admin_users.html:8
|
||||
msgid "Login name"
|
||||
msgstr "Anmeldename"
|
||||
|
||||
#: snikket_web/admin.py:68 snikket_web/templates/admin_delete_user.html:12
|
||||
#: snikket_web/templates/admin_users.html:9 snikket_web/user.py:61
|
||||
msgid "Display name"
|
||||
msgstr "Anzeigename"
|
||||
|
||||
#: snikket_web/admin.py:72 snikket_web/templates/admin_edit_user.html:33
|
||||
msgid "Access Level"
|
||||
msgstr "Berechtigungen"
|
||||
|
||||
#: snikket_web/admin.py:77
|
||||
msgid "Normal user"
|
||||
msgstr "Normaler Benutzer"
|
||||
|
||||
#: snikket_web/admin.py:78
|
||||
msgid "Administrator"
|
||||
msgstr "Administrator"
|
||||
|
||||
#: snikket_web/admin.py:83
|
||||
msgid "Update user"
|
||||
msgstr "Benutzer ändern"
|
||||
|
||||
#: snikket_web/admin.py:87
|
||||
msgid "Create password reset link"
|
||||
msgstr "Passwort-Link erzeugen"
|
||||
|
||||
#: snikket_web/admin.py:105
|
||||
msgid "Password reset link created"
|
||||
msgstr "Link zum Zurücksetzen des Passwortes erzeugt"
|
||||
|
||||
#: snikket_web/admin.py:120
|
||||
msgid "User information updated."
|
||||
msgstr "Benutzerinformationen gespeichert."
|
||||
|
||||
#: snikket_web/admin.py:142
|
||||
msgid "Delete user permanently"
|
||||
msgstr "Benutzer endgültig löschen"
|
||||
|
||||
#: snikket_web/admin.py:129
|
||||
#: snikket_web/admin.py:155
|
||||
msgid "User deleted"
|
||||
msgstr "Benutzer gelöscht"
|
||||
|
||||
#: snikket_web/admin.py:193
|
||||
msgid "Password reset link not found"
|
||||
msgstr "Link zum Zurücksetzen des Passwortes nicht gefunden"
|
||||
|
||||
#: snikket_web/admin.py:205
|
||||
msgid "Password reset link deleted"
|
||||
msgstr "Link gelöscht"
|
||||
|
||||
#: snikket_web/admin.py:225
|
||||
msgid "Invite to circle"
|
||||
msgstr "In Gemeinschaft einladen"
|
||||
|
||||
#: snikket_web/admin.py:135
|
||||
#: snikket_web/admin.py:231
|
||||
msgid "At least one circle must be selected"
|
||||
msgstr "Mindestens eine Gemeinschaft muss ausgewählt sein"
|
||||
|
||||
#: snikket_web/admin.py:140
|
||||
#: snikket_web/admin.py:236
|
||||
msgid "Valid for"
|
||||
msgstr "Gültig für"
|
||||
|
||||
#: snikket_web/admin.py:142
|
||||
#: snikket_web/admin.py:238
|
||||
msgid "One hour"
|
||||
msgstr "Eine Stunde"
|
||||
|
||||
#: snikket_web/admin.py:143
|
||||
#: snikket_web/admin.py:239
|
||||
msgid "Twelve hours"
|
||||
msgstr "Zwölf Stunden"
|
||||
|
||||
#: snikket_web/admin.py:144
|
||||
#: snikket_web/admin.py:240
|
||||
msgid "One day"
|
||||
msgstr "Ein Tag"
|
||||
|
||||
#: snikket_web/admin.py:145
|
||||
#: snikket_web/admin.py:241
|
||||
msgid "One week"
|
||||
msgstr "Eine Woche"
|
||||
|
||||
#: snikket_web/admin.py:146
|
||||
#: snikket_web/admin.py:242
|
||||
msgid "Four weeks"
|
||||
msgstr "Vier Wochen"
|
||||
|
||||
#: snikket_web/admin.py:152 snikket_web/templates/admin_edit_invite.html:17
|
||||
#: snikket_web/admin.py:248 snikket_web/templates/admin_edit_invite.html:17
|
||||
msgid "Invitation type"
|
||||
msgstr "Art der Einladung"
|
||||
|
||||
#: snikket_web/admin.py:154 snikket_web/templates/library.j2:116
|
||||
#: snikket_web/admin.py:250 snikket_web/templates/library.j2:116
|
||||
msgid "Individual"
|
||||
msgstr "Einzelperson"
|
||||
|
||||
#: snikket_web/admin.py:155 snikket_web/templates/library.j2:114
|
||||
#: snikket_web/admin.py:251 snikket_web/templates/library.j2:114
|
||||
msgid "Group"
|
||||
msgstr "Gruppe"
|
||||
|
||||
#: snikket_web/admin.py:161
|
||||
#: snikket_web/admin.py:257
|
||||
msgid "New invitation link"
|
||||
msgstr "Neuer Einladungslink"
|
||||
|
||||
#: snikket_web/admin.py:223
|
||||
#: snikket_web/admin.py:319
|
||||
msgid "Revoke"
|
||||
msgstr "Löschen"
|
||||
|
||||
#: snikket_web/admin.py:283 snikket_web/admin.py:327
|
||||
#: snikket_web/admin.py:343
|
||||
msgid "Invitation created"
|
||||
msgstr "Einladung angelegt"
|
||||
|
||||
#: snikket_web/admin.py:359
|
||||
msgid "No such invitation exists"
|
||||
msgstr "Diese Einladung existiert nicht"
|
||||
|
||||
#: snikket_web/admin.py:374
|
||||
msgid "Invitation revoked"
|
||||
msgstr "Einladung gelöscht"
|
||||
|
||||
#: snikket_web/admin.py:391 snikket_web/admin.py:439
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
|
||||
#: snikket_web/admin.py:288 snikket_web/templates/admin_circles.html:47
|
||||
#: snikket_web/admin.py:396 snikket_web/templates/admin_circles.html:47
|
||||
msgid "Create circle"
|
||||
msgstr "Gemeinschaft gründen"
|
||||
|
||||
#: snikket_web/admin.py:332
|
||||
#: snikket_web/admin.py:426
|
||||
msgid "Circle created"
|
||||
msgstr "Gemeinschaft gegründet"
|
||||
|
||||
#: snikket_web/admin.py:444
|
||||
msgid "Select user"
|
||||
msgstr "Benutzer auswählen"
|
||||
|
||||
#: snikket_web/admin.py:337
|
||||
#: snikket_web/admin.py:449
|
||||
msgid "Update circle"
|
||||
msgstr "Gemeinschaft ändern"
|
||||
|
||||
#: snikket_web/admin.py:341
|
||||
#: snikket_web/admin.py:453
|
||||
msgid "Delete circle permanently"
|
||||
msgstr "Gemeinschaft endgültig löschen"
|
||||
|
||||
#: snikket_web/admin.py:347
|
||||
#: snikket_web/admin.py:459
|
||||
msgid "Add user"
|
||||
msgstr "Benutzer hinzufügen"
|
||||
|
||||
#: snikket_web/infra.py:40
|
||||
#: snikket_web/admin.py:475
|
||||
msgid "No such circle exists"
|
||||
msgstr "Diese Gemeinschaft existiert nicht"
|
||||
|
||||
#: snikket_web/admin.py:512
|
||||
msgid "Circle data updated"
|
||||
msgstr "Gemeinschaftsdaten aktualisiert"
|
||||
|
||||
#: snikket_web/admin.py:518
|
||||
msgid "Circle deleted"
|
||||
msgstr "Gemeinschaft gelöscht"
|
||||
|
||||
#: snikket_web/admin.py:529
|
||||
msgid "User added to circle"
|
||||
msgstr "Benutzer zur Gemeinschaft hinzugefügt"
|
||||
|
||||
#: snikket_web/admin.py:538
|
||||
msgid "User removed from circle"
|
||||
msgstr "Benutzer aus der Gemeinschaft entfernt"
|
||||
|
||||
#: snikket_web/infra.py:41
|
||||
msgid "Main"
|
||||
msgstr "Kern"
|
||||
|
||||
#: snikket_web/invite.py:93
|
||||
#: snikket_web/invite.py:106
|
||||
msgid "Username"
|
||||
msgstr "Benutzername"
|
||||
|
||||
#: snikket_web/invite.py:97 snikket_web/invite.py:164 snikket_web/main.py:41
|
||||
#: snikket_web/invite.py:110 snikket_web/invite.py:177 snikket_web/main.py:41
|
||||
msgid "Password"
|
||||
msgstr "Passwort"
|
||||
|
||||
#: snikket_web/invite.py:101 snikket_web/invite.py:168
|
||||
#: snikket_web/invite.py:114 snikket_web/invite.py:181
|
||||
msgid "Confirm password"
|
||||
msgstr "Passwort (Bestätigung)"
|
||||
|
||||
#: snikket_web/invite.py:105 snikket_web/invite.py:172
|
||||
msgid "The passwords must match"
|
||||
msgstr "Die Passwörter müssen übereinstimmen"
|
||||
#: snikket_web/invite.py:118 snikket_web/invite.py:185
|
||||
msgid "The passwords must match."
|
||||
msgstr "Die Passwörter müssen übereinstimmen."
|
||||
|
||||
#: snikket_web/invite.py:110
|
||||
#: snikket_web/invite.py:123
|
||||
msgid "Create account"
|
||||
msgstr "Konto anlegen"
|
||||
|
||||
#: snikket_web/invite.py:137
|
||||
msgid "That username is already taken"
|
||||
msgstr "Dieser Benutzername ist bereits belegt"
|
||||
#: snikket_web/invite.py:150
|
||||
msgid "That username is already taken."
|
||||
msgstr "Dieser Benutzername ist bereits belegt."
|
||||
|
||||
#: snikket_web/invite.py:141 snikket_web/invite.py:205
|
||||
msgid "Registration was declined for unknown reasons"
|
||||
msgstr "Die Registrierung wurde aus unbekannten Gründen abgelehnt"
|
||||
#: snikket_web/invite.py:154 snikket_web/invite.py:218
|
||||
msgid "Registration was declined for unknown reasons."
|
||||
msgstr "Die Registrierung wurde aus unbekannten Gründen abgelehnt."
|
||||
|
||||
#: snikket_web/invite.py:145
|
||||
msgid "The username is not valid"
|
||||
msgstr "Der Benutzername ist ungültig"
|
||||
#: snikket_web/invite.py:158
|
||||
msgid "The username is not valid."
|
||||
msgstr "Der Benutzername ist ungültig."
|
||||
|
||||
#: snikket_web/invite.py:177 snikket_web/templates/user_home.html:32
|
||||
#: snikket_web/templates/user_passwd.html:32
|
||||
#: snikket_web/invite.py:190 snikket_web/templates/user_home.html:32
|
||||
#: snikket_web/templates/user_passwd.html:29
|
||||
msgid "Change password"
|
||||
msgstr "Passwort ändern"
|
||||
|
||||
@@ -149,69 +239,96 @@ msgstr "Adresse"
|
||||
msgid "Sign in"
|
||||
msgstr "Anmelden"
|
||||
|
||||
#: snikket_web/main.py:72
|
||||
#: snikket_web/main.py:55
|
||||
msgid "Invalid username or password."
|
||||
msgstr "Benutzername oder Passwort falsch."
|
||||
|
||||
#: snikket_web/user.py:21
|
||||
#: snikket_web/main.py:83
|
||||
msgid "Login successful!"
|
||||
msgstr "Anmeldung erfolgreich!"
|
||||
|
||||
#: snikket_web/user.py:27
|
||||
msgid "Current password"
|
||||
msgstr "Aktuelles Passwort"
|
||||
|
||||
#: snikket_web/user.py:26
|
||||
#: snikket_web/user.py:32
|
||||
msgid "New password"
|
||||
msgstr "Neues Passwort"
|
||||
|
||||
#: snikket_web/user.py:31
|
||||
#: snikket_web/user.py:37
|
||||
msgid "Confirm new password"
|
||||
msgstr "Neues Passwort (Bestätigung)"
|
||||
|
||||
#: snikket_web/user.py:35
|
||||
msgid "The new passwords must match"
|
||||
msgstr "Die neuen Passwörter müssen übereinstimmen"
|
||||
#: snikket_web/user.py:41
|
||||
msgid "The new passwords must match."
|
||||
msgstr "Die neuen Passwörter müssen übereinstimmen."
|
||||
|
||||
#: snikket_web/user.py:42
|
||||
#: snikket_web/user.py:48
|
||||
msgid "Sign out"
|
||||
msgstr "Abmelden"
|
||||
|
||||
#: snikket_web/user.py:47
|
||||
#: snikket_web/user.py:53
|
||||
msgid "Nobody"
|
||||
msgstr "Niemand"
|
||||
|
||||
#: snikket_web/user.py:48
|
||||
#: snikket_web/user.py:54
|
||||
msgid "Friends only"
|
||||
msgstr "Nur Freunde"
|
||||
|
||||
#: snikket_web/user.py:49
|
||||
#: snikket_web/user.py:55
|
||||
msgid "Everyone"
|
||||
msgstr "Jeder"
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:12
|
||||
#: snikket_web/templates/admin_users.html:11 snikket_web/user.py:55
|
||||
msgid "Display name"
|
||||
msgstr "Anzeigename"
|
||||
|
||||
#: snikket_web/user.py:59
|
||||
#: snikket_web/user.py:65
|
||||
msgid "Avatar"
|
||||
msgstr "Bild"
|
||||
|
||||
#: snikket_web/user.py:63
|
||||
#: snikket_web/user.py:69
|
||||
msgid "Profile visibility"
|
||||
msgstr "Profilsichtbarkeit"
|
||||
|
||||
#: snikket_web/user.py:68
|
||||
#: snikket_web/user.py:74
|
||||
msgid "Update profile"
|
||||
msgstr "Profil bearbeiten"
|
||||
|
||||
#: snikket_web/user.py:93
|
||||
msgid "Incorrect password"
|
||||
msgstr "Ungültiges Passwort"
|
||||
#: snikket_web/user.py:99
|
||||
msgid "Incorrect password."
|
||||
msgstr "Ungültiges Passwort."
|
||||
|
||||
#: snikket_web/templates/_footer.html:4 snikket_web/templates/login.html:36
|
||||
#: snikket_web/user.py:103
|
||||
msgid "Password changed"
|
||||
msgstr "Passwort geändert"
|
||||
|
||||
#: snikket_web/user.py:111
|
||||
msgid ""
|
||||
"The chosen avatar is too big. To be able to upload larger avatars, please "
|
||||
"use the app."
|
||||
msgstr ""
|
||||
"Das gewählte Profilbild ist zu groß. Benutze die App um größere Bilder "
|
||||
"hochladen zu können."
|
||||
|
||||
#: snikket_web/user.py:159
|
||||
msgid "Profile updated"
|
||||
msgstr "Profil gespeichert"
|
||||
|
||||
#: snikket_web/templates/unauth.html:18 snikket_web/user.py:167
|
||||
msgid "Error"
|
||||
msgstr "Fehler"
|
||||
|
||||
#: snikket_web/templates/_footer.html:4
|
||||
#, python-format
|
||||
msgid "A <a href=\"%(about_url)s\">Snikket</a> service"
|
||||
msgstr "Ein <a href=\"%(about_url)s\">Snikket</a>-Dienst"
|
||||
|
||||
#: snikket_web/templates/about.html:9
|
||||
#: snikket_web/templates/_footer.html:6
|
||||
msgid ""
|
||||
"“Snikket” and the parrot logo are trademarks of Snikket Community Interest "
|
||||
"Company."
|
||||
msgstr ""
|
||||
"„Snikket“ und das Papageienlogo sind Markenzeichen der Snikket Community "
|
||||
"Interest Company."
|
||||
|
||||
#: snikket_web/templates/about.html:4 snikket_web/templates/about.html:9
|
||||
msgid "About Snikket"
|
||||
msgstr "Über Snikket"
|
||||
|
||||
@@ -271,10 +388,25 @@ msgstr ""
|
||||
"\"%(apache20_url)s\">Apache 2.0 Lizenz</a> bereitgestellt werden."
|
||||
|
||||
#: snikket_web/templates/about.html:17
|
||||
msgid "Trademarks"
|
||||
msgstr "Markenzeichen"
|
||||
|
||||
#: snikket_web/templates/about.html:18
|
||||
#, python-format
|
||||
msgid ""
|
||||
"“Snikket” and the parrot logo are trademarks of Snikket Community Interest "
|
||||
"Company. For more information about the trademarks, visit the <a href="
|
||||
"\"%(trademarks_url)s\">Snikket Trademarks information page</a>."
|
||||
msgstr ""
|
||||
"„Snikket“ und das Papageienlogo sind Markenzeichen der Snikket Community "
|
||||
"Interest Company. Mehr Informationen über die Markenzeichen gibt es auf der "
|
||||
"<a href=\"%(trademarks_url)s\">„Snikket Trademarks“ Informationsseite</a>."
|
||||
|
||||
#: snikket_web/templates/about.html:19
|
||||
msgid "Software Versions"
|
||||
msgstr "Softwareversionen"
|
||||
|
||||
#: snikket_web/templates/about.html:27
|
||||
#: snikket_web/templates/about.html:29
|
||||
msgid "Back to the main page"
|
||||
msgstr "Zurück zur Hauptseite"
|
||||
|
||||
@@ -315,7 +447,7 @@ msgstr "Mitglieder"
|
||||
|
||||
#: snikket_web/templates/admin_circles.html:15
|
||||
#: snikket_web/templates/admin_invites.html:24
|
||||
#: snikket_web/templates/admin_users.html:12
|
||||
#: snikket_web/templates/admin_users.html:10
|
||||
msgid "Actions"
|
||||
msgstr "Aktionen"
|
||||
|
||||
@@ -353,7 +485,7 @@ msgstr "Neue Gemeinschaft"
|
||||
|
||||
#: snikket_web/templates/admin_create_invite.html:3
|
||||
msgid "Create invitation"
|
||||
msgstr "Gemeinschaft gründen"
|
||||
msgstr "Einladung erzeugen"
|
||||
|
||||
#: snikket_web/templates/admin_create_invite_form.html:5
|
||||
msgid "Create new invitation"
|
||||
@@ -373,7 +505,7 @@ msgid "Debug information for %(user_name)s"
|
||||
msgstr "Debugging-Informationen für %(user_name)s"
|
||||
|
||||
#: snikket_web/templates/admin_debug_user.html:11
|
||||
#: snikket_web/templates/user_passwd.html:26
|
||||
#: snikket_web/templates/user_passwd.html:23
|
||||
msgid "Warning"
|
||||
msgstr "Warnung"
|
||||
|
||||
@@ -390,12 +522,12 @@ msgid "Copy complete output"
|
||||
msgstr "Komplette Ausgabe kopieren"
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:4
|
||||
#: snikket_web/templates/admin_users.html:22
|
||||
#, python-format
|
||||
msgid "Delete user %(user_name)s"
|
||||
msgstr "Benutzer %(user_name)s löschen"
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:6
|
||||
#: snikket_web/templates/admin_edit_user.html:54
|
||||
msgid "Delete user"
|
||||
msgstr "Benutzer löschen"
|
||||
|
||||
@@ -403,11 +535,6 @@ msgstr "Benutzer löschen"
|
||||
msgid "Are you sure you want to delete the following user?"
|
||||
msgstr "Bist du sicher dass du den folgenden Benutzer löschen willst?"
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:10
|
||||
#: snikket_web/templates/admin_users.html:10
|
||||
msgid "Login name"
|
||||
msgstr "Anmeldename"
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:15
|
||||
msgid "Danger"
|
||||
msgstr "Gefahr"
|
||||
@@ -423,12 +550,10 @@ msgstr ""
|
||||
"keinen Weg zurück!</strong>"
|
||||
|
||||
#: 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:13
|
||||
#: snikket_web/templates/user_passwd.html:30
|
||||
#: snikket_web/templates/user_profile.html:31
|
||||
#: snikket_web/templates/user_logout.html:10
|
||||
#: snikket_web/templates/user_passwd.html:27
|
||||
#: snikket_web/templates/user_profile.html:32
|
||||
msgid "Back"
|
||||
msgstr "Zurück"
|
||||
|
||||
@@ -462,6 +587,10 @@ msgstr "Gemeinschaftsinformationen"
|
||||
msgid "This circle has no group chat associated."
|
||||
msgstr "Diese Gemeinschaft hat keinen zugehörigen Gruppenchat."
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:44
|
||||
msgid "Return to circle list"
|
||||
msgstr "Zurück zur Gemeinschaftsliste"
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:48
|
||||
msgid "Delete circle"
|
||||
msgstr "Gemeinschaft löschen"
|
||||
@@ -506,7 +635,6 @@ msgid "View invitation"
|
||||
msgstr "Einladung anzeigen"
|
||||
|
||||
#: snikket_web/templates/admin_edit_invite.html:13
|
||||
#: snikket_web/templates/admin_invites.html:21
|
||||
#: snikket_web/templates/admin_reset_user_password.html:15
|
||||
msgid "Valid until"
|
||||
msgstr "Gültig bis"
|
||||
@@ -552,6 +680,99 @@ msgstr "Der Benutzer wird als Kontakt von %(peer_jid)s hinzugefügt."
|
||||
msgid "Created"
|
||||
msgstr "Erzeugt"
|
||||
|
||||
#: snikket_web/templates/admin_edit_invite.html:48
|
||||
msgid "Return to invitation list"
|
||||
msgstr "Zurück zur Einladungsliste"
|
||||
|
||||
#: 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 ""
|
||||
"Eingeschränkte Benutzer können mit anderen Benutzern auf dem selben Snikket-"
|
||||
"Server interagieren und Mitglieder in Gemeinschaften sein."
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:7
|
||||
msgid ""
|
||||
"Like limited users and can also interact with users on other Snikket "
|
||||
"services."
|
||||
msgstr ""
|
||||
"Wie eingeschränkte Benutzer, zusätzlich können normale Benutzer auch mit "
|
||||
"Benutzern auf anderen Snikket-Servern interagieren."
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:9
|
||||
msgid "Like normal users and can access the admin panel in the web portal."
|
||||
msgstr ""
|
||||
"Wie normale Benutzer, zusätzlich können Administratoren auf den Adminbereich "
|
||||
"zugreifen."
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:20
|
||||
#: snikket_web/templates/admin_users.html:28
|
||||
#, python-format
|
||||
msgid "Edit user %(user_name)s"
|
||||
msgstr "Benutzer %(user_name)s bearbeiten"
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:23
|
||||
msgid "Edit user"
|
||||
msgstr "Benutzer bearbeiten"
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:27
|
||||
msgid "The login name cannot be changed."
|
||||
msgstr "Der Anmeldename kann nicht geändert werden."
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:34
|
||||
msgid ""
|
||||
"The access level of a user determines what interactions are allowed for them "
|
||||
"on your Snikket service."
|
||||
msgstr ""
|
||||
"Die Berechtigungen von Benutzern bestimmen, welche Interaktionen ihnen auf "
|
||||
"deinem Snikket-Server erlaubt sind."
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:41
|
||||
#, python-format
|
||||
msgid "<strong>%(title)s%(icon)s</strong><p>%(description)s</p>"
|
||||
msgstr "<strong>%(title)s%(icon)s</strong><p>%(description)s</p>"
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:51
|
||||
msgid "Return to user list"
|
||||
msgstr "Zurück zur Benutzerliste"
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:59
|
||||
msgid "Further actions"
|
||||
msgstr "Weitere Aktionen"
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:61
|
||||
msgid "Reset password"
|
||||
msgstr "Passwort zurücksetzen"
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:64
|
||||
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 ""
|
||||
"Wenn ein Benutzer das Passwort verloren oder vergessen hat, kannst du den "
|
||||
"folgenden Button verwenden um einen Link zu erzeugen. Dieser Link erlaubt es "
|
||||
"einmalig, das Passwort des Kontos zu ändern."
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:69
|
||||
msgid "Debug information"
|
||||
msgstr "Informationen zur Fehlerbehebung"
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:71
|
||||
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 ""
|
||||
"In seltenen Fällen kann es vorkommen, dass weiterführende Informationen über "
|
||||
"das Benutzerkonto sowie die verbundenen Geräte erforderlich sind um ein "
|
||||
"Problem zu beheben. Der folgende Button zeigt diese (teilweise vertraulichen)"
|
||||
" Informationen an."
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:75
|
||||
msgid "Show debug information"
|
||||
msgstr "Debugging-Informationen anzeigen"
|
||||
|
||||
#: snikket_web/templates/admin_home.html:4
|
||||
msgid "Welcome to the admin panel!"
|
||||
msgstr "Willkommen im Adminbereich!"
|
||||
@@ -604,6 +825,10 @@ msgstr "Adminbereich verlassen"
|
||||
msgid "Pending invitations"
|
||||
msgstr "Ausstehende Einladungen"
|
||||
|
||||
#: snikket_web/templates/admin_invites.html:21
|
||||
msgid "Expires"
|
||||
msgstr "Läuft ab"
|
||||
|
||||
#: snikket_web/templates/admin_invites.html:22
|
||||
msgid "Type"
|
||||
msgstr "Art"
|
||||
@@ -645,15 +870,21 @@ msgstr ""
|
||||
msgid "Destroy link"
|
||||
msgstr "Link zerstören"
|
||||
|
||||
#: snikket_web/templates/admin_users.html:25
|
||||
#, python-format
|
||||
msgid "Show debug information for %(user_name)s"
|
||||
msgstr "Debugging-Informationen für %(user_name)s anzeigen"
|
||||
#: snikket_web/templates/admin_users.html:19
|
||||
msgid "The user is an administrator."
|
||||
msgstr "Der Benutzer ist ein Administrator."
|
||||
|
||||
#: snikket_web/templates/admin_users.html:28
|
||||
#, python-format
|
||||
msgid "Create password reset link for %(user_name)s"
|
||||
msgstr "Benutzer %(user_name)s löschen"
|
||||
#: snikket_web/templates/admin_users.html:19
|
||||
msgid " (Administrator)"
|
||||
msgstr " (Administrator)"
|
||||
|
||||
#: snikket_web/templates/admin_users.html:22
|
||||
msgid "The user is restricted."
|
||||
msgstr "Dieser Benutzer ist eingeschränkt."
|
||||
|
||||
#: snikket_web/templates/admin_users.html:22
|
||||
msgid " (Restricted)"
|
||||
msgstr " (Eingeschränkt)"
|
||||
|
||||
#: snikket_web/templates/app.html:4
|
||||
msgid "Snikket Web Portal"
|
||||
@@ -689,7 +920,7 @@ msgid "The web portal encountered an internal error."
|
||||
msgstr "Das Webportal hatte einen internen Fehler."
|
||||
|
||||
#: snikket_web/templates/invite_invalid.html:5
|
||||
#: snikket_web/templates/invite_view.html:12
|
||||
#: snikket_web/templates/invite_view.html:13
|
||||
#, python-format
|
||||
msgid "Invite to %(site_name)s"
|
||||
msgstr "Einladung zu %(site_name)s"
|
||||
@@ -697,7 +928,7 @@ msgstr "Einladung zu %(site_name)s"
|
||||
#: snikket_web/templates/invite_invalid.html:6
|
||||
#: snikket_web/templates/invite_register.html:10
|
||||
#: snikket_web/templates/invite_success.html:11
|
||||
#: snikket_web/templates/invite_view.html:13
|
||||
#: snikket_web/templates/invite_view.html:14
|
||||
#, python-format
|
||||
msgid "Powered by <img src=\"%(logo_url)s\" alt=\"Snikket\">"
|
||||
msgstr "Betrieben mit <img alt=\"Snikket\" src=\"%(logo_url)s\">"
|
||||
@@ -744,18 +975,20 @@ msgstr ""
|
||||
"innerhalb der App zu machen. Das geht, indem du den folgenden Knopf drückst:"
|
||||
|
||||
#: snikket_web/templates/invite_register.html:14
|
||||
#: snikket_web/templates/invite_view.html:37
|
||||
#: snikket_web/templates/invite_view.html:39
|
||||
msgid "App already installed?"
|
||||
msgstr "App schon installiert?"
|
||||
|
||||
#: snikket_web/templates/invite_register.html:16
|
||||
#: snikket_web/templates/invite_reset_view.html:21
|
||||
#: snikket_web/templates/invite_view.html:39
|
||||
#: snikket_web/templates/invite_view.html:41
|
||||
#: snikket_web/templates/invite_view.html:106
|
||||
#: snikket_web/templates/invite_view.html:134
|
||||
msgid "Open the app"
|
||||
msgstr "App öffnen"
|
||||
|
||||
#: snikket_web/templates/invite_register.html:18
|
||||
#: snikket_web/templates/invite_view.html:41
|
||||
#: snikket_web/templates/invite_view.html:43
|
||||
msgid "This button works only if you have the app installed already!"
|
||||
msgstr "Dieser Knopf funktioniert nur, wenn du die App schon installiert hast!"
|
||||
|
||||
@@ -831,8 +1064,8 @@ msgid ""
|
||||
"This page allows you to reset the password of your account, <strong>"
|
||||
"%(account_jid)s</strong>, once."
|
||||
msgstr ""
|
||||
"Diese Seite erlaubt es dir einmalig, das Passwort deines Kontos "
|
||||
"<strong>%(account_jid)s</strong> zurückzusetzen."
|
||||
"Diese Seite erlaubt es dir einmalig, das Passwort deines Kontos <strong>"
|
||||
"%(account_jid)s</strong> zurückzusetzen."
|
||||
|
||||
#: snikket_web/templates/invite_reset_view.html:17
|
||||
msgid "Using the app"
|
||||
@@ -853,7 +1086,7 @@ msgstr ""
|
||||
"Das geht mit Hilfe des Scan-Knopfes in der oberen Leiste."
|
||||
|
||||
#: snikket_web/templates/invite_reset_view.html:26
|
||||
#: snikket_web/templates/invite_view.html:75
|
||||
#: 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 "
|
||||
@@ -869,7 +1102,7 @@ msgstr ""
|
||||
"Du wirst dann aufgefordert, ein neues Passwort für deinen Account einzugeben."
|
||||
|
||||
#: snikket_web/templates/invite_reset_view.html:29
|
||||
#: snikket_web/templates/invite_view.html:43
|
||||
#: snikket_web/templates/invite_view.html:45
|
||||
msgid "Alternatives"
|
||||
msgstr "Alternativen"
|
||||
|
||||
@@ -917,7 +1150,7 @@ msgstr ""
|
||||
msgid "Invite to %(site_name)s | Snikket"
|
||||
msgstr "Einladung zu %(site_name)s | Snikket"
|
||||
|
||||
#: snikket_web/templates/invite_view.html:15
|
||||
#: snikket_web/templates/invite_view.html:16
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You have been invited to chat with %(inviter_name)s using Snikket, a secure, "
|
||||
@@ -926,7 +1159,7 @@ msgstr ""
|
||||
"%(inviter_name)s hat dich zum Chatten mit Snikket auf %(site_name)s "
|
||||
"eingeladen, einer sicheren, privatsphärefreundlichen Chat-App."
|
||||
|
||||
#: snikket_web/templates/invite_view.html:17
|
||||
#: snikket_web/templates/invite_view.html:18
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You have been invited to chat on %(site_name)s using Snikket, a secure, "
|
||||
@@ -935,37 +1168,42 @@ msgstr ""
|
||||
"Dies ist eine Einladung zum Chatten auf %(site_name)s mit Snikket, einer "
|
||||
"sicheren, privatsphärefreundlichen Chat-App."
|
||||
|
||||
#: snikket_web/templates/invite_view.html:19
|
||||
#: snikket_web/templates/invite_view.html:20
|
||||
msgid "Get started"
|
||||
msgstr "Leg los!"
|
||||
|
||||
#: snikket_web/templates/invite_view.html:21
|
||||
#: snikket_web/templates/invite_view.html:22
|
||||
msgid "Install the Snikket App on your Android or iOS device."
|
||||
msgstr "Installiere die Snikket-App auf deinem Android- oder iOS-Gerät."
|
||||
|
||||
#: snikket_web/templates/invite_view.html:23
|
||||
#: snikket_web/templates/invite_view.html:24
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Install the Snikket App on your Android device (<a href=\"%(ios_info_url)s\" "
|
||||
"rel=\"noopener noreferrer\" target=\"_blank\">iOS coming soon!</a>)."
|
||||
msgstr ""
|
||||
"Installiere die Snikket-App auf deinem Android-Gerät (<a href=\""
|
||||
"%(ios_info_url)s\" rel=\"noopener noreferrer\" target=\"_blank\">iOS folgt "
|
||||
"Installiere die Snikket-App auf deinem Android-Gerät (<a href="
|
||||
"\"%(ios_info_url)s\" rel=\"noopener noreferrer\" target=\"_blank\">iOS folgt "
|
||||
"bald!</a>)."
|
||||
|
||||
#: snikket_web/templates/invite_view.html:27
|
||||
#: snikket_web/templates/invite_view.html:28
|
||||
msgid "Get it on Google Play"
|
||||
msgstr "Von Google Play installieren"
|
||||
|
||||
#: snikket_web/templates/invite_view.html:29
|
||||
#: snikket_web/templates/invite_view.html:30
|
||||
#: snikket_web/templates/invite_view.html:102
|
||||
msgid "Download on the App Store"
|
||||
msgstr "Laden im App Store"
|
||||
|
||||
#: snikket_web/templates/invite_view.html:33
|
||||
msgid "Not on mobile?"
|
||||
msgstr "Nicht am Smartphone?"
|
||||
#: snikket_web/templates/invite_view.html:32
|
||||
msgid "Get it on F-Droid"
|
||||
msgstr "Hole die App von F-Droid"
|
||||
|
||||
#: snikket_web/templates/invite_view.html:36
|
||||
#: snikket_web/templates/invite_view.html:35
|
||||
msgid "Send to mobile device"
|
||||
msgstr "An mobiles Gerät übertragen"
|
||||
|
||||
#: snikket_web/templates/invite_view.html:38
|
||||
msgid ""
|
||||
"After installation the app should automatically open and prompt you to "
|
||||
"create an account. If not, simply click the button below."
|
||||
@@ -974,7 +1212,7 @@ msgstr ""
|
||||
"ein Benutzerkonto anzulegen. Falls nicht, tippe einfach auf die folgende "
|
||||
"Schaltfläche."
|
||||
|
||||
#: snikket_web/templates/invite_view.html:44
|
||||
#: snikket_web/templates/invite_view.html:46
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You can connect to Snikket using any XMPP-compatible software. If the button "
|
||||
@@ -986,16 +1224,20 @@ msgstr ""
|
||||
"dass du <a href=\"%(register_url)s\">manuell ein Benutzerkonto anlegen</a> "
|
||||
"musst."
|
||||
|
||||
#: snikket_web/templates/invite_view.html:50
|
||||
#: snikket_web/templates/invite_view.html:52
|
||||
msgid "Scan invite code"
|
||||
msgstr "Einladungscode scannen"
|
||||
|
||||
#: snikket_web/templates/invite_view.html:53
|
||||
#: snikket_web/templates/invite_view.html:82
|
||||
#: snikket_web/templates/invite_view.html:55
|
||||
#: snikket_web/templates/invite_view.html:84
|
||||
#: snikket_web/templates/invite_view.html:96
|
||||
#: snikket_web/templates/invite_view.html:112
|
||||
#: snikket_web/templates/invite_view.html:124
|
||||
#: snikket_web/templates/invite_view.html:140
|
||||
msgid "Close"
|
||||
msgstr "Schließen"
|
||||
|
||||
#: snikket_web/templates/invite_view.html:56
|
||||
#: snikket_web/templates/invite_view.html:58
|
||||
msgid ""
|
||||
"You can transfer this invite to your mobile device by scanning a code with "
|
||||
"your camera. You can use either a QR scanner app or the Snikket app itself."
|
||||
@@ -1004,22 +1246,22 @@ msgstr ""
|
||||
"untenstehenden Code mit deiner Kamera scannst. Dafür kannst du entweder "
|
||||
"einen normalen QR-Scanner nehmen oder die Snikket-App selbst."
|
||||
|
||||
#: snikket_web/templates/invite_view.html:61
|
||||
#: snikket_web/templates/invite_view.html:63
|
||||
msgid "Using a QR code scanner"
|
||||
msgstr "Mit einem QR-Code-Scanner"
|
||||
|
||||
#: snikket_web/templates/invite_view.html:63
|
||||
#: snikket_web/templates/invite_view.html:65
|
||||
msgid "Using the Snikket app"
|
||||
msgstr "Mit der Snikket-App"
|
||||
|
||||
#: snikket_web/templates/invite_view.html:68
|
||||
#: 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 ""
|
||||
"Benutze einen <em>QR-Code</em>-Scanner auf deinem mobilen Gerät um den "
|
||||
"untenstehenden Code zu scannen:"
|
||||
|
||||
#: snikket_web/templates/invite_view.html:74
|
||||
#: 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."
|
||||
@@ -1027,6 +1269,51 @@ msgstr ""
|
||||
"Installiere die Snikket-App auf deinem mobilen Gerät, öffne sie und tippe "
|
||||
"dann auf den 'Scan'-Knopf in der oberen Leiste."
|
||||
|
||||
#: snikket_web/templates/invite_view.html:93
|
||||
msgid "Install on iOS"
|
||||
msgstr "Installation auf iOS"
|
||||
|
||||
#: snikket_web/templates/invite_view.html:99
|
||||
msgid ""
|
||||
"After downloading Snikket from the App Store, you have to return to this "
|
||||
"invite link and tap on \"Open the app\" to proceed."
|
||||
msgstr ""
|
||||
"Nachdem du Snikket vom App Store heruntergeladen hast musst du zu diesem "
|
||||
"Einladungslink zurückkehren und \"App öffnen\" antippen um fortzufahren."
|
||||
|
||||
#: snikket_web/templates/invite_view.html:101
|
||||
msgid "First download Snikket from the App Store using the button below:"
|
||||
msgstr ""
|
||||
"Lade zunächst Snikket aus dem App Store herunter indem du den folgenden "
|
||||
"Button benutzt:"
|
||||
|
||||
#: snikket_web/templates/invite_view.html:103
|
||||
#: snikket_web/templates/invite_view.html:131
|
||||
msgid ""
|
||||
"After the installation is complete, you can return to this page and tap the "
|
||||
"\"Open the app\" button to continue with the setup:"
|
||||
msgstr ""
|
||||
"Nachdem die Installation abgeschlossen ist kannst du zu dieser Seite "
|
||||
"zurückkehren und unten auf \"App öffnen\" tippen um die Einrichtung "
|
||||
"abzuschließen:"
|
||||
|
||||
#: snikket_web/templates/invite_view.html:121
|
||||
#: snikket_web/templates/invite_view.html:130
|
||||
msgid "Install via F-Droid"
|
||||
msgstr "Installation über F-Droid"
|
||||
|
||||
#: snikket_web/templates/invite_view.html:127
|
||||
msgid ""
|
||||
"After installing Snikket via F-Droid, you have to return to this invite link "
|
||||
"and tap on \"Open the app\" to proceed."
|
||||
msgstr ""
|
||||
"Nachdem du Snikket über F-Droid installiert hast, musst du auf diese Seite "
|
||||
"zurückkehren und \"App öffnen\" antippen um fortzufahren."
|
||||
|
||||
#: snikket_web/templates/invite_view.html:129
|
||||
msgid "First install Snikket from F-Droid using the button below:"
|
||||
msgstr "Installiere Snikket zunächst aus F-Droid mit dem folgenden Button:"
|
||||
|
||||
#: snikket_web/templates/library.j2:18
|
||||
msgid "Copy link"
|
||||
msgstr "Link kopieren"
|
||||
@@ -1060,9 +1347,22 @@ msgid "Enter your Snikket address and password to manage your account."
|
||||
msgstr ""
|
||||
"Gib deine Snikket-Adresse und -Passwort ein um dein Konto zu verwalten."
|
||||
|
||||
#: snikket_web/templates/login.html:18
|
||||
msgid "Login failed"
|
||||
msgstr "Anmeldung fehlgeschlagen"
|
||||
#: snikket_web/templates/login.html:19
|
||||
msgid "Incorrect address"
|
||||
msgstr "Ungültige Adresse"
|
||||
|
||||
#: snikket_web/templates/login.html:20
|
||||
#, python-format
|
||||
msgid ""
|
||||
"This Snikket service only hosts addresses ending in <em>@%(snikket_domain)s</"
|
||||
"em>. Your password was not sent."
|
||||
msgstr ""
|
||||
"Dieser Snikket-Server ist nur für Adressen, die auf <em>@%(snikket_domain)s</"
|
||||
"em> enden zuständig. Dein Passwort wurde nicht abgesendet."
|
||||
|
||||
#: snikket_web/templates/unauth.html:16
|
||||
msgid "Operation successful"
|
||||
msgstr "Aktion erfolgreich"
|
||||
|
||||
#: snikket_web/templates/user_home.html:9
|
||||
msgid "Welcome!"
|
||||
@@ -1098,11 +1398,11 @@ msgstr ""
|
||||
msgid "Admin panel"
|
||||
msgstr "Adminbereich"
|
||||
|
||||
#: snikket_web/templates/user_logout.html:8
|
||||
#: snikket_web/templates/user_logout.html:5
|
||||
msgid "Sign out of the Snikket Web Portal"
|
||||
msgstr "Aus dem Webportal abmelden"
|
||||
|
||||
#: snikket_web/templates/user_logout.html:9
|
||||
#: snikket_web/templates/user_logout.html:6
|
||||
msgid ""
|
||||
"Click below to log yourself out of the web portal. This does not affect any "
|
||||
"other connected devices."
|
||||
@@ -1110,11 +1410,11 @@ msgstr ""
|
||||
"Klicke unten um dich aus dem Webportal abzumelden. Dies betrifft keine "
|
||||
"anderen Geräte von dir."
|
||||
|
||||
#: snikket_web/templates/user_passwd.html:8
|
||||
#: snikket_web/templates/user_passwd.html:5
|
||||
msgid "Change your password"
|
||||
msgstr "Ändere dein Passwort"
|
||||
|
||||
#: snikket_web/templates/user_passwd.html:9
|
||||
#: snikket_web/templates/user_passwd.html:6
|
||||
msgid ""
|
||||
"To change your password, you need to provide the current password as well as "
|
||||
"the new one. To reduce the chance of typos, we ask for your new password "
|
||||
@@ -1124,7 +1424,7 @@ msgstr ""
|
||||
"dein neues Passwort angeben. Damit Tippfehler dich nicht aussperren bitten "
|
||||
"wir dich, dein neues Passwort zweimal einzutippen."
|
||||
|
||||
#: snikket_web/templates/user_passwd.html:27
|
||||
#: snikket_web/templates/user_passwd.html:24
|
||||
msgid ""
|
||||
"After changing your password, you will have to enter the new password on all "
|
||||
"of your devices."
|
||||
@@ -1132,19 +1432,19 @@ msgstr ""
|
||||
"Nachdem du das Passwort geändert hast, musst du das neue Passwort auf allen "
|
||||
"Geräten manuell eintragen."
|
||||
|
||||
#: snikket_web/templates/user_profile.html:7
|
||||
#: snikket_web/templates/user_profile.html:4
|
||||
msgid "Update your profile"
|
||||
msgstr "Dein Profil bearbeiten"
|
||||
|
||||
#: snikket_web/templates/user_profile.html:9
|
||||
#: snikket_web/templates/user_profile.html:6
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
|
||||
#: snikket_web/templates/user_profile.html:22
|
||||
#: snikket_web/templates/user_profile.html:23
|
||||
msgid "Visibility"
|
||||
msgstr "Sichtbarkeit"
|
||||
|
||||
#: snikket_web/templates/user_profile.html:23
|
||||
#: snikket_web/templates/user_profile.html:24
|
||||
msgid ""
|
||||
"This section allows you to control who can see your profile information, "
|
||||
"like avatar and nickname."
|
||||
@@ -1152,23 +1452,14 @@ msgstr ""
|
||||
"Hier kannst du einstellen, wer deine Profilinformationen, wie Bild oder "
|
||||
"Anzeigename einsehen kann."
|
||||
|
||||
#~ msgid "Edit user %(user_name)s"
|
||||
#~ msgstr "Benutzer %(user_name)s bearbeiten"
|
||||
#~ msgid "Create password reset link for %(user_name)s"
|
||||
#~ msgstr "Link zum Zurücksetzen des Passwortes von %(user_name)s erzeugen"
|
||||
|
||||
#~ msgid "User information"
|
||||
#~ msgstr "Benutzerinformationen"
|
||||
#~ msgid "Login failed"
|
||||
#~ msgstr "Anmeldung fehlgeschlagen"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "If the user has forgotten their password, use the below button to create "
|
||||
#~ "a password reset link. The password reset link can be used once to change "
|
||||
#~ "the password of the account. Transmit the link to the user via a secure "
|
||||
#~ "channel."
|
||||
#~ msgstr ""
|
||||
#~ "Wenn ein Benutzer das Passwort vergessen hat, kannst du den folgenden "
|
||||
#~ "Button benutzen um einen Link zum Zurücksetzen des Passworts zu erzeugen. "
|
||||
#~ "Dieser Link kann dann ein einziges Mal verwendet werden um das Passwort "
|
||||
#~ "des Kontos zu ändern. Lasse den Link dem Benutzer auf einem sicheren "
|
||||
#~ "Übertragungsweg zukommen."
|
||||
#~ msgid "Not on mobile?"
|
||||
#~ msgstr "Nicht am Smartphone?"
|
||||
|
||||
#~ msgid "No users left"
|
||||
#~ msgstr "Keine Benutzer übrig"
|
||||
@@ -1235,8 +1526,5 @@ msgstr ""
|
||||
#~ msgid "Copy operation failed"
|
||||
#~ msgstr "Kopieren fehlgeschlagen"
|
||||
|
||||
#~ msgid "Password change failed"
|
||||
#~ msgstr "Passwortänderung fehlgeschlagen"
|
||||
|
||||
#~ msgid "Allow multiple uses"
|
||||
#~ msgstr "Mehrfach verwendbar"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
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.
1398
snikket_web/translations/es_MX/LC_MESSAGES/messages.po
Normal file
1398
snikket_web/translations/es_MX/LC_MESSAGES/messages.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
snikket_web/translations/fr/LC_MESSAGES/messages.mo
Normal file
BIN
snikket_web/translations/fr/LC_MESSAGES/messages.mo
Normal file
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
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2021-01-30 12:45+0100\n"
|
||||
"POT-Creation-Date: 2021-03-25 17:32+0100\n"
|
||||
"PO-Revision-Date: 2021-01-28 17:55+0000\n"
|
||||
"Last-Translator: pep <pep@bouah.net>\n"
|
||||
"Language-Team: Japanese <https://i18n.sotecware.net/projects/snikket/web-"
|
||||
@@ -20,124 +20,254 @@ msgstr ""
|
||||
"X-Generator: Weblate 4.4.2\n"
|
||||
"Generated-By: Babel 2.9.0\n"
|
||||
|
||||
#: snikket_web/admin.py:60
|
||||
#: snikket_web/admin.py:59
|
||||
msgid "Limited"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:64 snikket_web/templates/admin_delete_user.html:10
|
||||
#: snikket_web/templates/admin_users.html:8
|
||||
msgid "Login name"
|
||||
msgstr "ロゲイン名"
|
||||
|
||||
#: snikket_web/admin.py:68 snikket_web/templates/admin_delete_user.html:12
|
||||
#: snikket_web/templates/admin_users.html:9 snikket_web/user.py:61
|
||||
msgid "Display name"
|
||||
msgstr "表示名"
|
||||
|
||||
#: snikket_web/admin.py:72 snikket_web/templates/admin_edit_user.html:33
|
||||
msgid "Access Level"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:77
|
||||
msgid "Normal user"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:78
|
||||
#, fuzzy
|
||||
#| msgid "Admin area"
|
||||
msgid "Administrator"
|
||||
msgstr "管理"
|
||||
|
||||
#: snikket_web/admin.py:83
|
||||
#, fuzzy
|
||||
#| msgid "Update circle"
|
||||
msgid "Update user"
|
||||
msgstr "サークルを更新"
|
||||
|
||||
#: snikket_web/admin.py:87
|
||||
#, fuzzy
|
||||
#| msgid "Password reset link for %(user_name)s"
|
||||
msgid "Create password reset link"
|
||||
msgstr "%(user_name)s のパスワード再設定リンク"
|
||||
|
||||
#: snikket_web/admin.py:105
|
||||
#, fuzzy
|
||||
#| msgid "Password reset link for %(user_name)s"
|
||||
msgid "Password reset link created"
|
||||
msgstr "%(user_name)s のパスワード再設定リンク"
|
||||
|
||||
#: snikket_web/admin.py:120
|
||||
#, fuzzy
|
||||
#| msgid "User information"
|
||||
msgid "User information updated."
|
||||
msgstr "ユーザー詳細"
|
||||
|
||||
#: snikket_web/admin.py:142
|
||||
msgid "Delete user permanently"
|
||||
msgstr "ユーザーを削除する"
|
||||
|
||||
#: snikket_web/admin.py:129
|
||||
#: snikket_web/admin.py:155
|
||||
msgid "User deleted"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:193
|
||||
#, fuzzy
|
||||
#| msgid "Password reset link for %(user_name)s"
|
||||
msgid "Password reset link not found"
|
||||
msgstr "%(user_name)s のパスワード再設定リンク"
|
||||
|
||||
#: snikket_web/admin.py:205
|
||||
#, fuzzy
|
||||
#| msgid "Password reset link for %(user_name)s"
|
||||
msgid "Password reset link deleted"
|
||||
msgstr "%(user_name)s のパスワード再設定リンク"
|
||||
|
||||
#: snikket_web/admin.py:225
|
||||
msgid "Invite to circle"
|
||||
msgstr "サークルに紹介する"
|
||||
|
||||
#: snikket_web/admin.py:135
|
||||
#: snikket_web/admin.py:231
|
||||
msgid "At least one circle must be selected"
|
||||
msgstr "サークルを選択してください"
|
||||
|
||||
#: snikket_web/admin.py:140
|
||||
#: snikket_web/admin.py:236
|
||||
msgid "Valid for"
|
||||
msgstr "有効期限"
|
||||
|
||||
#: snikket_web/admin.py:142
|
||||
#: snikket_web/admin.py:238
|
||||
msgid "One hour"
|
||||
msgstr "一時間"
|
||||
|
||||
#: snikket_web/admin.py:143
|
||||
#: snikket_web/admin.py:239
|
||||
msgid "Twelve hours"
|
||||
msgstr "12時間"
|
||||
|
||||
#: snikket_web/admin.py:144
|
||||
#: snikket_web/admin.py:240
|
||||
msgid "One day"
|
||||
msgstr "一日"
|
||||
|
||||
#: snikket_web/admin.py:145
|
||||
#: snikket_web/admin.py:241
|
||||
msgid "One week"
|
||||
msgstr "一週間"
|
||||
|
||||
#: snikket_web/admin.py:146
|
||||
#: snikket_web/admin.py:242
|
||||
msgid "Four weeks"
|
||||
msgstr "4週間"
|
||||
|
||||
#: snikket_web/admin.py:152 snikket_web/templates/admin_edit_invite.html:17
|
||||
#: snikket_web/admin.py:248 snikket_web/templates/admin_edit_invite.html:17
|
||||
msgid "Invitation type"
|
||||
msgstr "紹介の種類"
|
||||
|
||||
#: snikket_web/admin.py:154 snikket_web/templates/library.j2:116
|
||||
#: snikket_web/admin.py:250 snikket_web/templates/library.j2:116
|
||||
msgid "Individual"
|
||||
msgstr "一回"
|
||||
|
||||
#: snikket_web/admin.py:155 snikket_web/templates/library.j2:114
|
||||
#: snikket_web/admin.py:251 snikket_web/templates/library.j2:114
|
||||
msgid "Group"
|
||||
msgstr "複数回"
|
||||
|
||||
#: snikket_web/admin.py:161
|
||||
#: snikket_web/admin.py:257
|
||||
msgid "New invitation link"
|
||||
msgstr "新しい紹介状"
|
||||
|
||||
#: snikket_web/admin.py:223
|
||||
#: snikket_web/admin.py:319
|
||||
msgid "Revoke"
|
||||
msgstr "取り消す"
|
||||
|
||||
#: snikket_web/admin.py:283 snikket_web/admin.py:327
|
||||
#: snikket_web/admin.py:343
|
||||
#, fuzzy
|
||||
#| msgid "Invitation type"
|
||||
msgid "Invitation created"
|
||||
msgstr "紹介の種類"
|
||||
|
||||
#: snikket_web/admin.py:359
|
||||
#, fuzzy
|
||||
#| msgid "New invitation link"
|
||||
msgid "No such invitation exists"
|
||||
msgstr "新しい紹介状"
|
||||
|
||||
#: snikket_web/admin.py:374
|
||||
#, fuzzy
|
||||
#| msgid "Invitation type"
|
||||
msgid "Invitation revoked"
|
||||
msgstr "紹介の種類"
|
||||
|
||||
#: snikket_web/admin.py:391 snikket_web/admin.py:439
|
||||
msgid "Name"
|
||||
msgstr "名"
|
||||
|
||||
#: snikket_web/admin.py:288 snikket_web/templates/admin_circles.html:47
|
||||
#: snikket_web/admin.py:396 snikket_web/templates/admin_circles.html:47
|
||||
msgid "Create circle"
|
||||
msgstr "サークルを作成"
|
||||
|
||||
#: snikket_web/admin.py:332
|
||||
#: snikket_web/admin.py:426
|
||||
#, fuzzy
|
||||
#| msgid "Circle name"
|
||||
msgid "Circle created"
|
||||
msgstr "サークル名"
|
||||
|
||||
#: snikket_web/admin.py:444
|
||||
msgid "Select user"
|
||||
msgstr "ユーザー選択"
|
||||
|
||||
#: snikket_web/admin.py:337
|
||||
#: snikket_web/admin.py:449
|
||||
msgid "Update circle"
|
||||
msgstr "サークルを更新"
|
||||
|
||||
#: snikket_web/admin.py:341
|
||||
#: snikket_web/admin.py:453
|
||||
msgid "Delete circle permanently"
|
||||
msgstr "サークルを削除"
|
||||
|
||||
#: snikket_web/admin.py:347
|
||||
#: snikket_web/admin.py:459
|
||||
msgid "Add user"
|
||||
msgstr "ユーザーを追加する"
|
||||
|
||||
#: snikket_web/infra.py:40
|
||||
#: snikket_web/admin.py:475
|
||||
#, fuzzy
|
||||
#| msgid "No circles"
|
||||
msgid "No such circle exists"
|
||||
msgstr "なし"
|
||||
|
||||
#: snikket_web/admin.py:512
|
||||
#, fuzzy
|
||||
#| msgid "Circle name"
|
||||
msgid "Circle data updated"
|
||||
msgstr "サークル名"
|
||||
|
||||
#: snikket_web/admin.py:518
|
||||
#, fuzzy
|
||||
#| msgid "Circle members"
|
||||
msgid "Circle deleted"
|
||||
msgstr "サークル会員"
|
||||
|
||||
#: snikket_web/admin.py:529
|
||||
#, fuzzy
|
||||
#| msgid "Invite to circle"
|
||||
msgid "User added to circle"
|
||||
msgstr "サークルに紹介する"
|
||||
|
||||
#: snikket_web/admin.py:538
|
||||
#, fuzzy
|
||||
#| msgid "Remove user %(username)s from circle"
|
||||
msgid "User removed from circle"
|
||||
msgstr "%(username)s をサークルから外す"
|
||||
|
||||
#: snikket_web/infra.py:41
|
||||
msgid "Main"
|
||||
msgstr "第一サークル"
|
||||
|
||||
#: snikket_web/invite.py:93
|
||||
#: snikket_web/invite.py:106
|
||||
msgid "Username"
|
||||
msgstr "ユーザー名"
|
||||
|
||||
#: snikket_web/invite.py:97 snikket_web/invite.py:164 snikket_web/main.py:41
|
||||
#: snikket_web/invite.py:110 snikket_web/invite.py:177 snikket_web/main.py:41
|
||||
msgid "Password"
|
||||
msgstr "パスワード"
|
||||
|
||||
#: snikket_web/invite.py:101 snikket_web/invite.py:168
|
||||
#: snikket_web/invite.py:114 snikket_web/invite.py:181
|
||||
msgid "Confirm password"
|
||||
msgstr "確認用パスワード"
|
||||
|
||||
#: snikket_web/invite.py:105 snikket_web/invite.py:172
|
||||
msgid "The passwords must match"
|
||||
#: snikket_web/invite.py:118 snikket_web/invite.py:185
|
||||
#, fuzzy
|
||||
#| msgid "The passwords must match"
|
||||
msgid "The passwords must match."
|
||||
msgstr "確認用パスワードが一致しません"
|
||||
|
||||
#: snikket_web/invite.py:110
|
||||
#: snikket_web/invite.py:123
|
||||
msgid "Create account"
|
||||
msgstr "アカウント作成"
|
||||
|
||||
#: snikket_web/invite.py:137
|
||||
msgid "That username is already taken"
|
||||
#: snikket_web/invite.py:150
|
||||
#, fuzzy
|
||||
#| msgid "That username is already taken"
|
||||
msgid "That username is already taken."
|
||||
msgstr "このユーザー名は存在しています"
|
||||
|
||||
#: snikket_web/invite.py:141 snikket_web/invite.py:205
|
||||
msgid "Registration was declined for unknown reasons"
|
||||
#: snikket_web/invite.py:154 snikket_web/invite.py:218
|
||||
#, fuzzy
|
||||
#| msgid "Registration was declined for unknown reasons"
|
||||
msgid "Registration was declined for unknown reasons."
|
||||
msgstr "理由不明の登録エラー"
|
||||
|
||||
#: snikket_web/invite.py:145
|
||||
msgid "The username is not valid"
|
||||
#: snikket_web/invite.py:158
|
||||
#, fuzzy
|
||||
#| msgid "The username is not valid"
|
||||
msgid "The username is not valid."
|
||||
msgstr "ユーザー名が不正"
|
||||
|
||||
#: snikket_web/invite.py:177 snikket_web/templates/user_home.html:32
|
||||
#: snikket_web/templates/user_passwd.html:32
|
||||
#: snikket_web/invite.py:190 snikket_web/templates/user_home.html:32
|
||||
#: snikket_web/templates/user_passwd.html:29
|
||||
msgid "Change password"
|
||||
msgstr "パスワード変更"
|
||||
|
||||
@@ -149,69 +279,100 @@ msgstr "アドレス"
|
||||
msgid "Sign in"
|
||||
msgstr "サインイン"
|
||||
|
||||
#: snikket_web/main.py:72
|
||||
#: snikket_web/main.py:55
|
||||
msgid "Invalid username or password."
|
||||
msgstr "ユーザー名またはパスワードが不正。"
|
||||
|
||||
#: snikket_web/user.py:21
|
||||
#: snikket_web/main.py:83
|
||||
msgid "Login successful!"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:27
|
||||
msgid "Current password"
|
||||
msgstr "現在のパスワード"
|
||||
|
||||
#: snikket_web/user.py:26
|
||||
#: snikket_web/user.py:32
|
||||
msgid "New password"
|
||||
msgstr "新しいパスワード"
|
||||
|
||||
#: snikket_web/user.py:31
|
||||
#: snikket_web/user.py:37
|
||||
msgid "Confirm new password"
|
||||
msgstr "新しいパスワードの確認"
|
||||
|
||||
#: snikket_web/user.py:35
|
||||
msgid "The new passwords must match"
|
||||
#: snikket_web/user.py:41
|
||||
#, fuzzy
|
||||
#| msgid "The new passwords must match"
|
||||
msgid "The new passwords must match."
|
||||
msgstr "新しいパスワードが不一致"
|
||||
|
||||
#: snikket_web/user.py:42
|
||||
#: snikket_web/user.py:48
|
||||
msgid "Sign out"
|
||||
msgstr "サインアウト"
|
||||
|
||||
#: snikket_web/user.py:47
|
||||
#: snikket_web/user.py:53
|
||||
msgid "Nobody"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:48
|
||||
#: snikket_web/user.py:54
|
||||
msgid "Friends only"
|
||||
msgstr "コンタクト限定"
|
||||
|
||||
#: snikket_web/user.py:49
|
||||
#: snikket_web/user.py:55
|
||||
msgid "Everyone"
|
||||
msgstr "全員"
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:12
|
||||
#: snikket_web/templates/admin_users.html:11 snikket_web/user.py:55
|
||||
msgid "Display name"
|
||||
msgstr "表示名"
|
||||
|
||||
#: snikket_web/user.py:59
|
||||
#: snikket_web/user.py:65
|
||||
msgid "Avatar"
|
||||
msgstr "アバター"
|
||||
|
||||
#: snikket_web/user.py:63
|
||||
#: snikket_web/user.py:69
|
||||
msgid "Profile visibility"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:68
|
||||
#: snikket_web/user.py:74
|
||||
msgid "Update profile"
|
||||
msgstr "プロファイル管理"
|
||||
|
||||
#: snikket_web/user.py:93
|
||||
msgid "Incorrect password"
|
||||
#: snikket_web/user.py:99
|
||||
#, fuzzy
|
||||
#| msgid "Incorrect password"
|
||||
msgid "Incorrect password."
|
||||
msgstr "パスワード不正"
|
||||
|
||||
#: snikket_web/templates/_footer.html:4 snikket_web/templates/login.html:36
|
||||
#: snikket_web/user.py:103
|
||||
#, fuzzy
|
||||
#| msgid "Password reset"
|
||||
msgid "Password changed"
|
||||
msgstr "パスワード再設定"
|
||||
|
||||
#: snikket_web/user.py:111
|
||||
msgid ""
|
||||
"The chosen avatar is too big. To be able to upload larger avatars, please "
|
||||
"use the app."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:159
|
||||
#, fuzzy
|
||||
#| msgid "Profile"
|
||||
msgid "Profile updated"
|
||||
msgstr "プロファイル"
|
||||
|
||||
#: snikket_web/templates/unauth.html:18 snikket_web/user.py:167
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/_footer.html:4
|
||||
#, python-format
|
||||
msgid "A <a href=\"%(about_url)s\">Snikket</a> service"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/about.html:9
|
||||
#: snikket_web/templates/_footer.html:6
|
||||
msgid ""
|
||||
"“Snikket” and the parrot logo are trademarks of Snikket Community Interest "
|
||||
"Company."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/about.html:4 snikket_web/templates/about.html:9
|
||||
msgid "About Snikket"
|
||||
msgstr "Snikketについて"
|
||||
|
||||
@@ -262,10 +423,22 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/about.html:17
|
||||
msgid "Trademarks"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/about.html:18
|
||||
#, python-format
|
||||
msgid ""
|
||||
"“Snikket” and the parrot logo are trademarks of Snikket Community Interest "
|
||||
"Company. For more information about the trademarks, visit the <a href="
|
||||
"\"%(trademarks_url)s\">Snikket Trademarks information page</a>."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/about.html:19
|
||||
msgid "Software Versions"
|
||||
msgstr "ソフトウェアバーション"
|
||||
|
||||
#: snikket_web/templates/about.html:27
|
||||
#: snikket_web/templates/about.html:29
|
||||
msgid "Back to the main page"
|
||||
msgstr "戻る"
|
||||
|
||||
@@ -301,7 +474,7 @@ msgstr "サークル員"
|
||||
|
||||
#: snikket_web/templates/admin_circles.html:15
|
||||
#: snikket_web/templates/admin_invites.html:24
|
||||
#: snikket_web/templates/admin_users.html:12
|
||||
#: snikket_web/templates/admin_users.html:10
|
||||
msgid "Actions"
|
||||
msgstr "操作"
|
||||
|
||||
@@ -355,7 +528,7 @@ msgid "Debug information for %(user_name)s"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_debug_user.html:11
|
||||
#: snikket_web/templates/user_passwd.html:26
|
||||
#: snikket_web/templates/user_passwd.html:23
|
||||
msgid "Warning"
|
||||
msgstr ""
|
||||
|
||||
@@ -372,12 +545,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 "%(user_name)s を削除"
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:6
|
||||
#: snikket_web/templates/admin_edit_user.html:54
|
||||
msgid "Delete user"
|
||||
msgstr "ユーザー削除"
|
||||
|
||||
@@ -385,11 +558,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 "警告"
|
||||
@@ -404,12 +572,10 @@ msgstr ""
|
||||
"strong>"
|
||||
|
||||
#: 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:13
|
||||
#: snikket_web/templates/user_passwd.html:30
|
||||
#: snikket_web/templates/user_profile.html:31
|
||||
#: snikket_web/templates/user_logout.html:10
|
||||
#: snikket_web/templates/user_passwd.html:27
|
||||
#: snikket_web/templates/user_profile.html:32
|
||||
msgid "Back"
|
||||
msgstr "戻る"
|
||||
|
||||
@@ -441,6 +607,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 "サークル削除"
|
||||
@@ -483,7 +653,6 @@ msgid "View invitation"
|
||||
msgstr "紹介状表示"
|
||||
|
||||
#: snikket_web/templates/admin_edit_invite.html:13
|
||||
#: snikket_web/templates/admin_invites.html:21
|
||||
#: snikket_web/templates/admin_reset_user_password.html:15
|
||||
msgid "Valid until"
|
||||
msgstr "有効期限"
|
||||
@@ -525,6 +694,96 @@ msgstr ""
|
||||
msgid "Created"
|
||||
msgstr "作成時"
|
||||
|
||||
#: snikket_web/templates/admin_edit_invite.html:48
|
||||
#, fuzzy
|
||||
#| msgid "New invitation link"
|
||||
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 "%(user_name)s を更新"
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:23
|
||||
#, fuzzy
|
||||
#| msgid "Add user"
|
||||
msgid "Edit user"
|
||||
msgstr "ユーザーを追加する"
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:27
|
||||
msgid "The login name cannot be changed."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:34
|
||||
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:41
|
||||
#, python-format
|
||||
msgid "<strong>%(title)s%(icon)s</strong><p>%(description)s</p>"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:51
|
||||
#, fuzzy
|
||||
#| msgid "New invitation link"
|
||||
msgid "Return to user list"
|
||||
msgstr "新しい紹介状"
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:59
|
||||
msgid "Further actions"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:61
|
||||
#, fuzzy
|
||||
#| msgid "Change your password"
|
||||
msgid "Reset password"
|
||||
msgstr "パスワード変更"
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:64
|
||||
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:69
|
||||
#, fuzzy
|
||||
#| msgid "User information"
|
||||
msgid "Debug information"
|
||||
msgstr "ユーザー詳細"
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:71
|
||||
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:75
|
||||
#, fuzzy
|
||||
#| msgid "User information"
|
||||
msgid "Show debug information"
|
||||
msgstr "ユーザー詳細"
|
||||
|
||||
#: snikket_web/templates/admin_home.html:4
|
||||
#, fuzzy
|
||||
#| msgid "Back to the main page"
|
||||
@@ -588,6 +847,10 @@ msgstr ""
|
||||
msgid "Pending invitations"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_invites.html:21
|
||||
msgid "Expires"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_invites.html:22
|
||||
msgid "Type"
|
||||
msgstr "種類"
|
||||
@@ -627,14 +890,24 @@ 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_users.html:19
|
||||
#, fuzzy
|
||||
#| msgid "The username is not valid"
|
||||
msgid "The user is an administrator."
|
||||
msgstr "ユーザー名が不正"
|
||||
|
||||
#: snikket_web/templates/admin_users.html:19
|
||||
msgid " (Administrator)"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_users.html:28
|
||||
#, python-format
|
||||
msgid "Create password reset link for %(user_name)s"
|
||||
#: snikket_web/templates/admin_users.html:22
|
||||
#, fuzzy
|
||||
#| msgid "The username is not valid"
|
||||
msgid "The user is restricted."
|
||||
msgstr "ユーザー名が不正"
|
||||
|
||||
#: snikket_web/templates/admin_users.html:22
|
||||
msgid " (Restricted)"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/app.html:4
|
||||
@@ -669,7 +942,7 @@ msgid "The web portal encountered an internal error."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_invalid.html:5
|
||||
#: snikket_web/templates/invite_view.html:12
|
||||
#: snikket_web/templates/invite_view.html:13
|
||||
#, python-format
|
||||
msgid "Invite to %(site_name)s"
|
||||
msgstr "%(site_name)s に紹介する"
|
||||
@@ -677,7 +950,7 @@ msgstr "%(site_name)s に紹介する"
|
||||
#: snikket_web/templates/invite_invalid.html:6
|
||||
#: snikket_web/templates/invite_register.html:10
|
||||
#: snikket_web/templates/invite_success.html:11
|
||||
#: snikket_web/templates/invite_view.html:13
|
||||
#: snikket_web/templates/invite_view.html:14
|
||||
#, python-format
|
||||
msgid "Powered by <img src=\"%(logo_url)s\" alt=\"Snikket\">"
|
||||
msgstr "Powered by <img alt=\"Snikket\" src=\"%(logo_url)s\">"
|
||||
@@ -720,18 +993,20 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_register.html:14
|
||||
#: snikket_web/templates/invite_view.html:37
|
||||
#: snikket_web/templates/invite_view.html:39
|
||||
msgid "App already installed?"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_register.html:16
|
||||
#: snikket_web/templates/invite_reset_view.html:21
|
||||
#: snikket_web/templates/invite_view.html:39
|
||||
#: snikket_web/templates/invite_view.html:41
|
||||
#: snikket_web/templates/invite_view.html:106
|
||||
#: snikket_web/templates/invite_view.html:134
|
||||
msgid "Open the app"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_register.html:18
|
||||
#: snikket_web/templates/invite_view.html:41
|
||||
#: snikket_web/templates/invite_view.html:43
|
||||
msgid "This button works only if you have the app installed already!"
|
||||
msgstr ""
|
||||
|
||||
@@ -824,7 +1099,7 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_reset_view.html:26
|
||||
#: snikket_web/templates/invite_view.html:75
|
||||
#: 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 "
|
||||
@@ -836,7 +1111,7 @@ msgid "You will then be prompted to enter a new password for your account."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_reset_view.html:29
|
||||
#: snikket_web/templates/invite_view.html:43
|
||||
#: snikket_web/templates/invite_view.html:45
|
||||
msgid "Alternatives"
|
||||
msgstr ""
|
||||
|
||||
@@ -881,54 +1156,59 @@ msgstr ""
|
||||
msgid "Invite to %(site_name)s | Snikket"
|
||||
msgstr "%(site_name)s に紹介 | Snikket"
|
||||
|
||||
#: snikket_web/templates/invite_view.html:15
|
||||
#: snikket_web/templates/invite_view.html:16
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You have been invited to chat with %(inviter_name)s using Snikket, a secure, "
|
||||
"privacy-friendly chat app on %(site_name)s."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:17
|
||||
#: snikket_web/templates/invite_view.html:18
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You have been invited to chat on %(site_name)s using Snikket, a secure, "
|
||||
"privacy-friendly chat app."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:19
|
||||
#: snikket_web/templates/invite_view.html:20
|
||||
msgid "Get started"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:21
|
||||
#: snikket_web/templates/invite_view.html:22
|
||||
msgid "Install the Snikket App on your Android or iOS device."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:23
|
||||
#: snikket_web/templates/invite_view.html:24
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Install the Snikket App on your Android device (<a href=\"%(ios_info_url)s\" "
|
||||
"rel=\"noopener noreferrer\" target=\"_blank\">iOS coming soon!</a>)."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:27
|
||||
#: snikket_web/templates/invite_view.html:28
|
||||
msgid "Get it on Google Play"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:29
|
||||
#: snikket_web/templates/invite_view.html:30
|
||||
#: snikket_web/templates/invite_view.html:102
|
||||
msgid "Download on the App Store"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:33
|
||||
msgid "Not on mobile?"
|
||||
#: snikket_web/templates/invite_view.html:32
|
||||
msgid "Get it on F-Droid"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:36
|
||||
#: snikket_web/templates/invite_view.html:35
|
||||
msgid "Send to mobile device"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:38
|
||||
msgid ""
|
||||
"After installation the app should automatically open and prompt you to "
|
||||
"create an account. If not, simply click the button below."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:44
|
||||
#: snikket_web/templates/invite_view.html:46
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You can connect to Snikket using any XMPP-compatible software. If the button "
|
||||
@@ -936,40 +1216,80 @@ msgid ""
|
||||
"\">register an account manually</a>."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:50
|
||||
#: snikket_web/templates/invite_view.html:52
|
||||
msgid "Scan invite code"
|
||||
msgstr "紹介状をスキャン"
|
||||
|
||||
#: snikket_web/templates/invite_view.html:53
|
||||
#: snikket_web/templates/invite_view.html:82
|
||||
#: snikket_web/templates/invite_view.html:55
|
||||
#: snikket_web/templates/invite_view.html:84
|
||||
#: snikket_web/templates/invite_view.html:96
|
||||
#: snikket_web/templates/invite_view.html:112
|
||||
#: snikket_web/templates/invite_view.html:124
|
||||
#: snikket_web/templates/invite_view.html:140
|
||||
msgid "Close"
|
||||
msgstr "閉める"
|
||||
|
||||
#: snikket_web/templates/invite_view.html:56
|
||||
#: snikket_web/templates/invite_view.html:58
|
||||
msgid ""
|
||||
"You can transfer this invite to your mobile device by scanning a code with "
|
||||
"your camera. You can use either a QR scanner app or the Snikket app itself."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:61
|
||||
#: snikket_web/templates/invite_view.html:63
|
||||
msgid "Using a QR code scanner"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:63
|
||||
#: snikket_web/templates/invite_view.html:65
|
||||
msgid "Using the Snikket app"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:68
|
||||
#: 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:74
|
||||
#: 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
|
||||
msgid "Install on iOS"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:99
|
||||
msgid ""
|
||||
"After downloading Snikket from the App Store, you have to return to this "
|
||||
"invite link and tap on \"Open the app\" to proceed."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:101
|
||||
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
|
||||
msgid ""
|
||||
"After the installation is complete, you can return to this page and tap the "
|
||||
"\"Open the app\" button to continue with the setup:"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:121
|
||||
#: snikket_web/templates/invite_view.html:130
|
||||
msgid "Install via F-Droid"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:127
|
||||
msgid ""
|
||||
"After installing Snikket via F-Droid, you have to return to this invite link "
|
||||
"and tap on \"Open the app\" to proceed."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_view.html:129
|
||||
msgid "First install Snikket from F-Droid using the button below:"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/library.j2:18
|
||||
msgid "Copy link"
|
||||
msgstr "リンクをコピーする"
|
||||
@@ -998,8 +1318,21 @@ msgstr "Snikket ログイン"
|
||||
msgid "Enter your Snikket address and password to manage your account."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/login.html:18
|
||||
msgid "Login failed"
|
||||
#: snikket_web/templates/login.html:19
|
||||
#, fuzzy
|
||||
#| msgid "Incorrect password"
|
||||
msgid "Incorrect address"
|
||||
msgstr "パスワード不正"
|
||||
|
||||
#: snikket_web/templates/login.html:20
|
||||
#, python-format
|
||||
msgid ""
|
||||
"This Snikket service only hosts addresses ending in <em>@%(snikket_domain)s</"
|
||||
"em>. Your password was not sent."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/unauth.html:16
|
||||
msgid "Operation successful"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/user_home.html:9
|
||||
@@ -1045,59 +1378,53 @@ msgstr ""
|
||||
msgid "Admin panel"
|
||||
msgstr "管理"
|
||||
|
||||
#: snikket_web/templates/user_logout.html:8
|
||||
#: snikket_web/templates/user_logout.html:5
|
||||
msgid "Sign out of the Snikket Web Portal"
|
||||
msgstr "Snikket ホームページからログアウト"
|
||||
|
||||
#: snikket_web/templates/user_logout.html:9
|
||||
#: snikket_web/templates/user_logout.html:6
|
||||
msgid ""
|
||||
"Click below to log yourself out of the web portal. This does not affect any "
|
||||
"other connected devices."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/user_passwd.html:8
|
||||
#: snikket_web/templates/user_passwd.html:5
|
||||
msgid "Change your password"
|
||||
msgstr "パスワード変更"
|
||||
|
||||
#: snikket_web/templates/user_passwd.html:9
|
||||
#: snikket_web/templates/user_passwd.html:6
|
||||
msgid ""
|
||||
"To change your password, you need to provide the current password as well as "
|
||||
"the new one. To reduce the chance of typos, we ask for your new password "
|
||||
"twice."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/user_passwd.html:27
|
||||
#: snikket_web/templates/user_passwd.html:24
|
||||
msgid ""
|
||||
"After changing your password, you will have to enter the new password on all "
|
||||
"of your devices."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/user_profile.html:7
|
||||
#: snikket_web/templates/user_profile.html:4
|
||||
#, fuzzy
|
||||
#| msgid "Update profile"
|
||||
msgid "Update your profile"
|
||||
msgstr "プロファイル管理"
|
||||
|
||||
#: snikket_web/templates/user_profile.html:9
|
||||
#: snikket_web/templates/user_profile.html:6
|
||||
msgid "Profile"
|
||||
msgstr "プロファイル"
|
||||
|
||||
#: snikket_web/templates/user_profile.html:22
|
||||
#: snikket_web/templates/user_profile.html:23
|
||||
msgid "Visibility"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/user_profile.html:23
|
||||
#: snikket_web/templates/user_profile.html:24
|
||||
msgid ""
|
||||
"This section allows you to control who can see your profile information, "
|
||||
"like avatar and nickname."
|
||||
msgstr ""
|
||||
|
||||
#~ msgid "Edit user %(user_name)s"
|
||||
#~ msgstr "%(user_name)s を更新"
|
||||
|
||||
#~ msgid "User information"
|
||||
#~ msgstr "ユーザー詳細"
|
||||
|
||||
#~ msgid "This circle cannot be modified"
|
||||
#~ msgstr "このサークルの更新ができません"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
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
BIN
snikket_web/translations/sv/LC_MESSAGES/messages.mo
Normal file
BIN
snikket_web/translations/sv/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
1431
snikket_web/translations/sv/LC_MESSAGES/messages.po
Normal file
1431
snikket_web/translations/sv/LC_MESSAGES/messages.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,21 +2,27 @@ import asyncio
|
||||
import typing
|
||||
|
||||
import quart.flask_patch
|
||||
from quart import Blueprint, render_template, request, redirect, url_for
|
||||
from quart import (
|
||||
Blueprint,
|
||||
render_template,
|
||||
request,
|
||||
redirect,
|
||||
url_for,
|
||||
flash,
|
||||
current_app,
|
||||
)
|
||||
import quart.exceptions
|
||||
|
||||
import wtforms
|
||||
|
||||
import flask_wtf
|
||||
|
||||
from flask_babel import lazy_gettext as _l, _
|
||||
|
||||
from .infra import client
|
||||
from .infra import client, BaseForm
|
||||
|
||||
bp = Blueprint('user', __name__)
|
||||
|
||||
|
||||
class ChangePasswordForm(flask_wtf.FlaskForm): # type:ignore
|
||||
class ChangePasswordForm(BaseForm):
|
||||
current_password = wtforms.PasswordField(
|
||||
_l("Current password"),
|
||||
validators=[wtforms.validators.InputRequired()]
|
||||
@@ -32,12 +38,12 @@ class ChangePasswordForm(flask_wtf.FlaskForm): # type:ignore
|
||||
validators=[wtforms.validators.InputRequired(),
|
||||
wtforms.validators.EqualTo(
|
||||
"new_password",
|
||||
_l("The new passwords must match")
|
||||
_l("The new passwords must match.")
|
||||
)]
|
||||
)
|
||||
|
||||
|
||||
class LogoutForm(flask_wtf.FlaskForm): # type:ignore
|
||||
class LogoutForm(BaseForm):
|
||||
action_signout = wtforms.SubmitField(
|
||||
_l("Sign out"),
|
||||
)
|
||||
@@ -50,7 +56,7 @@ _ACCESS_MODEL_CHOICES = [
|
||||
]
|
||||
|
||||
|
||||
class ProfileForm(flask_wtf.FlaskForm): # type:ignore
|
||||
class ProfileForm(BaseForm):
|
||||
nickname = wtforms.TextField(
|
||||
_l("Display name"),
|
||||
)
|
||||
@@ -90,17 +96,29 @@ async def change_pw() -> typing.Union[str, quart.Response]:
|
||||
quart.exceptions.Forbidden):
|
||||
# server refused current password, set an appropriate error
|
||||
form.current_password.errors.append(
|
||||
_("Incorrect password"),
|
||||
_("Incorrect password."),
|
||||
)
|
||||
else:
|
||||
await flash(
|
||||
_("Password changed"),
|
||||
"success",
|
||||
)
|
||||
return redirect(url_for("user.change_pw"))
|
||||
|
||||
return await render_template("user_passwd.html", form=form)
|
||||
|
||||
|
||||
EAVATARTOOBIG = _l(
|
||||
"The chosen avatar is too big. To be able to upload larger "
|
||||
"avatars, please use the app."
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/profile", methods=["GET", "POST"])
|
||||
@client.require_session()
|
||||
async def profile() -> typing.Union[str, quart.Response]:
|
||||
max_avatar_size = current_app.config["MAX_AVATAR_SIZE"]
|
||||
|
||||
form = ProfileForm()
|
||||
if request.method != "POST":
|
||||
user_info = await client.get_user_info()
|
||||
@@ -114,26 +132,40 @@ async def profile() -> typing.Union[str, quart.Response]:
|
||||
if form.validate_on_submit():
|
||||
user_info = await client.get_user_info()
|
||||
|
||||
ok = True
|
||||
file_info = (await request.files).get(form.avatar.name)
|
||||
if file_info is not None:
|
||||
mimetype = file_info.mimetype
|
||||
data = file_info.stream.read()
|
||||
if len(data) > 0:
|
||||
if len(data) > max_avatar_size:
|
||||
print(len(data), max_avatar_size)
|
||||
form.avatar.errors.append(EAVATARTOOBIG)
|
||||
ok = False
|
||||
elif len(data) > 0:
|
||||
await client.set_user_avatar(data, mimetype)
|
||||
|
||||
if user_info.get("nickname") != form.nickname.data:
|
||||
await client.set_user_nickname(form.nickname.data)
|
||||
if ok:
|
||||
if user_info.get("nickname") != form.nickname.data:
|
||||
await client.set_user_nickname(form.nickname.data)
|
||||
|
||||
access_model = form.profile_access_model.data
|
||||
await asyncio.gather(
|
||||
client.set_avatar_access_model(access_model),
|
||||
client.set_vcard_access_model(access_model),
|
||||
client.set_nickname_access_model(access_model),
|
||||
)
|
||||
access_model = form.profile_access_model.data
|
||||
await asyncio.gather(
|
||||
client.set_avatar_access_model(access_model),
|
||||
client.set_vcard_access_model(access_model),
|
||||
client.set_nickname_access_model(access_model),
|
||||
)
|
||||
|
||||
return redirect(url_for(".profile"))
|
||||
await flash(
|
||||
_("Profile updated"),
|
||||
"success",
|
||||
)
|
||||
return redirect(url_for(".profile"))
|
||||
|
||||
return await render_template("user_profile.html", form=form)
|
||||
return await render_template("user_profile.html",
|
||||
form=form,
|
||||
max_avatar_size=max_avatar_size,
|
||||
avatar_too_big_warning_header=_l("Error"),
|
||||
avatar_too_big_warning=EAVATARTOOBIG)
|
||||
|
||||
|
||||
@bp.route("/logout", methods=["GET", "POST"])
|
||||
@@ -142,6 +174,12 @@ async def logout() -> typing.Union[quart.Response, str]:
|
||||
form = LogoutForm()
|
||||
if form.validate_on_submit():
|
||||
await client.logout()
|
||||
# No flashing here because we don’t collect flashes in the login page
|
||||
# and it’d be weird.
|
||||
# await flash(
|
||||
# _("Logged out"),
|
||||
# "success",
|
||||
# )
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
return await render_template("user_logout.html", form=form)
|
||||
|
||||
@@ -5,13 +5,16 @@ action/delete:delete
|
||||
action/logout:logout
|
||||
action/login:login
|
||||
action/exit_to_app:exit_to_app
|
||||
action/lock:lock
|
||||
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
|
||||
navigation/arrow_back:back
|
||||
navigation/arrow_forward:forward
|
||||
navigation/cancel:cancel
|
||||
@@ -25,3 +28,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