You've already forked snikket-web-portal
Compare commits
135 Commits
feature/fa
...
beta.20210
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
10a0de0637 | ||
|
|
b3185a8d18 | ||
|
|
2db6cbe6fd | ||
|
|
9bc6e0b555 | ||
|
|
98a3eeba7c | ||
|
|
de97b08f01 | ||
|
|
f2dc970731 | ||
|
|
2f8e724104 | ||
|
|
a3ab537de0 | ||
|
|
b04c4fa42d | ||
|
|
078be4ba35 | ||
|
|
c1f186a3da | ||
|
|
7aaeb0f368 | ||
|
|
b475e76189 | ||
|
|
8b6f5e8e18 |
23
.github/workflows/build-portal-image.yml
vendored
23
.github/workflows/build-portal-image.yml
vendored
@@ -1,23 +0,0 @@
|
||||
name: Docker image build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build the Docker image
|
||||
run: >-
|
||||
docker build . \
|
||||
--build-arg=BUILD_SERIES=dev \
|
||||
--build-arg=BUILD_ID="$(echo "$GITHUB_SHA" | head -c 12)" \
|
||||
--tag snikket/snikket-web-portal:dev
|
||||
- name: Log into registry
|
||||
run: echo "${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}" | docker login -u snikket --password-stdin
|
||||
- name: Push the Docker image
|
||||
run: docker push snikket/snikket-web-portal:dev
|
||||
31
.github/workflows/build-portal-release-image.yml
vendored
31
.github/workflows/build-portal-release-image.yml
vendored
@@ -1,31 +0,0 @@
|
||||
---
|
||||
name: Docker release image build
|
||||
|
||||
"on":
|
||||
push:
|
||||
tags:
|
||||
- release/*.*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build the Docker image
|
||||
run: >-
|
||||
echo "Building ref $GITHUB_REF...";
|
||||
RELEASE_TAG="${GITHUB_REF#refs/tags/release/}";
|
||||
RELEASE_SERIES="${RELEASE_TAG%.*}";
|
||||
RELEASE_VER="${RELEASE_TAG#$RELEASE_SERIES.}";
|
||||
docker build . \
|
||||
--build-arg=BUILD_SERIES="$RELEASE_SERIES" \
|
||||
--build-arg=BUILD_ID="$RELEASE_VER" \
|
||||
--tag snikket/snikket-web-portal:"$RELEASE_SERIES"
|
||||
- name: Log into registry
|
||||
run: echo "${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}" | docker login -u snikket --password-stdin
|
||||
- name: Push the Docker image
|
||||
run: >-
|
||||
RELEASE_TAG="${GITHUB_REF#refs/tags/release/}";
|
||||
RELEASE_SERIES="${RELEASE_TAG%.*}";
|
||||
docker push snikket/snikket-web-portal:"$RELEASE_SERIES"
|
||||
9
.github/workflows/main.yaml
vendored
9
.github/workflows/main.yaml
vendored
@@ -48,3 +48,12 @@ jobs:
|
||||
- name: Linting
|
||||
run: |
|
||||
python -m flake8 snikket_web
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build the Docker image
|
||||
run: >-
|
||||
docker build .
|
||||
|
||||
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.15
|
||||
flask-wtf~=0.14
|
||||
hsluv~=0.0.2
|
||||
flask-babel~=1.0
|
||||
|
||||
@@ -17,6 +17,7 @@ from quart import (
|
||||
redirect,
|
||||
jsonify,
|
||||
)
|
||||
import werkzeug.exceptions
|
||||
|
||||
import environ
|
||||
|
||||
@@ -40,7 +41,7 @@ async def proc() -> typing.Dict[str, typing.Any]:
|
||||
|
||||
try:
|
||||
user_info = await infra.client.get_user_info()
|
||||
except (aiohttp.ClientError, quart.exceptions.HTTPException):
|
||||
except (aiohttp.ClientError, werkzeug.exceptions.HTTPException):
|
||||
user_info = {}
|
||||
|
||||
return {
|
||||
@@ -48,6 +49,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,
|
||||
}
|
||||
|
||||
|
||||
@@ -104,16 +106,16 @@ async def backend_error_handler(exc: Exception) -> quart.Response:
|
||||
|
||||
|
||||
async def generic_http_error(
|
||||
exc: quart.exceptions.HTTPException,
|
||||
exc: werkzeug.exceptions.HTTPException,
|
||||
) -> quart.Response:
|
||||
return quart.Response(
|
||||
await render_template(
|
||||
"generic_http_error.html",
|
||||
status=exc.status_code,
|
||||
status=exc.code,
|
||||
description=exc.description,
|
||||
name=exc.name,
|
||||
),
|
||||
status=exc.status_code,
|
||||
status=exc.code,
|
||||
)
|
||||
|
||||
|
||||
@@ -143,8 +145,22 @@ class AppConfig:
|
||||
domain = environ.var()
|
||||
site_name = environ.var("")
|
||||
avatar_cache_ttl = environ.var(1800, converter=int)
|
||||
languages = environ.var(["de", "en"], converter=autosplit)
|
||||
languages = environ.var([
|
||||
"da",
|
||||
"de",
|
||||
"en",
|
||||
"fr",
|
||||
"id",
|
||||
"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)
|
||||
|
||||
|
||||
_UPPER_CASE = "".join(map(chr, range(ord("A"), ord("Z")+1)))
|
||||
@@ -175,29 +191,30 @@ 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.context_processor(proc)
|
||||
app.register_error_handler(
|
||||
aiohttp.ClientConnectorError,
|
||||
backend_error_handler, # type:ignore
|
||||
backend_error_handler,
|
||||
)
|
||||
app.register_error_handler(
|
||||
quart.exceptions.HTTPException,
|
||||
generic_http_error, # type:ignore
|
||||
werkzeug.exceptions.HTTPException,
|
||||
generic_http_error,
|
||||
)
|
||||
app.register_error_handler(
|
||||
Exception,
|
||||
generic_error_handler, # type:ignore
|
||||
generic_error_handler,
|
||||
)
|
||||
|
||||
@app.route("/")
|
||||
@app.route("/") # type: ignore
|
||||
async def index() -> quart.Response:
|
||||
if infra.client.has_session:
|
||||
return redirect(url_for('user.index'))
|
||||
|
||||
return redirect(url_for('main.login'))
|
||||
|
||||
@app.route("/site.webmanifest")
|
||||
@app.route("/site.webmanifest") # type: ignore
|
||||
def site_manifest() -> quart.Response:
|
||||
# this is needed for icons
|
||||
return jsonify(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
version_info = (0, 1, 0, "a0")
|
||||
version_info = (0, 1, 2, "a0")
|
||||
version = (
|
||||
".".join(map(str, version_info[:3])) +
|
||||
(f"-{version_info[3]}" if version_info[3] else "")
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import asyncio
|
||||
import json
|
||||
import typing
|
||||
|
||||
@@ -18,50 +17,133 @@ from quart import (
|
||||
url_for,
|
||||
request,
|
||||
abort,
|
||||
flash,
|
||||
)
|
||||
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 .infra import client, circle_name, BaseForm
|
||||
|
||||
bp = Blueprint("admin", __name__, url_prefix="/admin")
|
||||
|
||||
|
||||
@bp.route("/")
|
||||
@bp.route("/") # type:ignore
|
||||
@client.require_admin_session()
|
||||
async def index() -> str:
|
||||
return await render_template("admin_home.html")
|
||||
|
||||
|
||||
class PasswordResetLinkPost(flask_wtf.FlaskForm): # type: ignore
|
||||
action_create = wtforms.StringField()
|
||||
class PasswordResetLinkPost(BaseForm):
|
||||
action_revoke = wtforms.StringField()
|
||||
|
||||
|
||||
@bp.route("/users")
|
||||
@bp.route("/users") # type:ignore
|
||||
@client.require_admin_session()
|
||||
async def users() -> str:
|
||||
users = sorted(
|
||||
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"]) # type:ignore
|
||||
@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")
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/user/<localpart>/delete", methods=["GET", "POST"])
|
||||
@bp.route("/user/<localpart>/delete", methods=["GET", "POST"]) # type:ignore
|
||||
@client.require_admin_session()
|
||||
async def delete_user(localpart: str) -> typing.Union[str, quart.Response]:
|
||||
target_user_info = await client.get_user_by_localpart(localpart)
|
||||
@@ -69,6 +151,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(
|
||||
@@ -78,7 +164,7 @@ async def delete_user(localpart: str) -> typing.Union[str, quart.Response]:
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/user/<localpart>/debug")
|
||||
@bp.route("/user/<localpart>/debug") # type:ignore
|
||||
@client.require_admin_session()
|
||||
async def debug_user(localpart: str) -> typing.Union[str, quart.Response]:
|
||||
target_user_info = await client.get_user_by_localpart(localpart)
|
||||
@@ -94,37 +180,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"]) # type:ignore # noqa:E501
|
||||
@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.
|
||||
@@ -178,7 +274,7 @@ class InvitePost(flask_wtf.FlaskForm): # type:ignore
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/invitations", methods=["GET", "POST"])
|
||||
@bp.route("/invitations", methods=["GET", "POST"]) # type:ignore
|
||||
@client.require_admin_session()
|
||||
async def invitations() -> typing.Union[str, quart.Response]:
|
||||
invites = sorted(
|
||||
@@ -218,13 +314,13 @@ async def invitations() -> typing.Union[str, quart.Response]:
|
||||
)
|
||||
|
||||
|
||||
class InviteForm(flask_wtf.FlaskForm): # type:ignore
|
||||
class InviteForm(BaseForm):
|
||||
action_revoke = wtforms.SubmitField(
|
||||
_l("Revoke")
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/invitation/-/new", methods=["POST"])
|
||||
@bp.route("/invitation/-/new", methods=["POST"]) # type:ignore
|
||||
@client.require_admin_session()
|
||||
async def create_invite() -> typing.Union[str, quart.Response]:
|
||||
form = InvitePost()
|
||||
@@ -243,19 +339,27 @@ 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)
|
||||
|
||||
|
||||
@bp.route("/invitation/<id_>", methods=["GET", "POST"])
|
||||
@bp.route("/invitation/<id_>", methods=["GET", "POST"]) # type:ignore
|
||||
@client.require_admin_session()
|
||||
async def edit_invite(id_: str) -> typing.Union[str, quart.Response]:
|
||||
try:
|
||||
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 +370,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 +386,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()],
|
||||
@@ -289,7 +397,7 @@ class CirclePost(flask_wtf.FlaskForm): # type:ignore
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/circles")
|
||||
@bp.route("/circles") # type:ignore
|
||||
@client.require_admin_session()
|
||||
async def circles() -> str:
|
||||
circles = sorted(
|
||||
@@ -306,7 +414,7 @@ async def circles() -> str:
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/circle/-/new", methods=["POST"])
|
||||
@bp.route("/circle/-/new", methods=["POST"]) # type:ignore
|
||||
@client.require_admin_session()
|
||||
async def create_circle() -> typing.Union[str, quart.Response]:
|
||||
create_form = CirclePost()
|
||||
@@ -314,6 +422,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 +434,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()],
|
||||
@@ -348,7 +460,7 @@ class EditCircleForm(flask_wtf.FlaskForm): # type:ignore
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/circle/<id_>", methods=["GET", "POST"])
|
||||
@bp.route("/circle/<id_>", methods=["GET", "POST"]) # type:ignore
|
||||
@client.require_admin_session()
|
||||
async def edit_circle(id_: str) -> typing.Union[str, quart.Response]:
|
||||
async with client.authenticated_session() as session:
|
||||
@@ -359,28 +471,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 +508,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:
|
||||
|
||||
@@ -10,6 +10,7 @@ from quart import (
|
||||
)
|
||||
|
||||
import flask_babel
|
||||
import flask_wtf
|
||||
from flask_babel import _
|
||||
|
||||
from . import prosodyclient
|
||||
@@ -25,7 +26,7 @@ babel = flask_babel.Babel()
|
||||
def selected_locale() -> str:
|
||||
selected = request.accept_languages.best_match(
|
||||
current_app.config['LANGUAGES']
|
||||
)
|
||||
) or current_app.config['LANGUAGES'][0]
|
||||
return selected
|
||||
|
||||
|
||||
@@ -55,3 +56,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__)
|
||||
@@ -47,14 +46,21 @@ def context() -> typing.Mapping[str, typing.Any]:
|
||||
}
|
||||
|
||||
|
||||
@bp.route("/<id_>")
|
||||
async def view(id_: str) -> str:
|
||||
@bp.route("/<id_>") # type:ignore
|
||||
async def view_old(id_: str) -> quart.Response:
|
||||
return redirect(url_for(".view", id_=id_))
|
||||
|
||||
|
||||
@bp.route("/<id_>/") # type:ignore
|
||||
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.")
|
||||
)]
|
||||
)
|
||||
|
||||
@@ -111,7 +124,7 @@ class RegisterForm(flask_wtf.FlaskForm): # type:ignore
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/<id_>/register", methods=["GET", "POST"])
|
||||
@bp.route("/<id_>/register", methods=["GET", "POST"]) # type:ignore
|
||||
async def register(id_: str) -> typing.Union[str, quart.Response]:
|
||||
try:
|
||||
invite = await client.get_public_invite_by_id(id_)
|
||||
@@ -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.")
|
||||
)]
|
||||
)
|
||||
|
||||
@@ -178,7 +191,7 @@ class ResetForm(flask_wtf.FlaskForm): # type:ignore
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/<id_>/reset", methods=["GET", "POST"])
|
||||
@bp.route("/<id_>/reset", methods=["GET", "POST"]) # type:ignore
|
||||
async def reset(id_: str) -> typing.Union[str, quart.Response]:
|
||||
try:
|
||||
invite = await client.get_public_invite_by_id(id_)
|
||||
@@ -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_))
|
||||
@@ -219,7 +232,7 @@ async def reset(id_: str) -> typing.Union[str, quart.Response]:
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/success", methods=["GET", "POST"])
|
||||
@bp.route("/success", methods=["GET", "POST"]) # type:ignore
|
||||
async def success() -> str:
|
||||
return await render_template(
|
||||
"invite_success.html",
|
||||
@@ -227,7 +240,7 @@ async def success() -> str:
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/success/reset", methods=["GET", "POST"])
|
||||
@bp.route("/success/reset", methods=["GET", "POST"]) # type:ignore
|
||||
async def reset_success() -> str:
|
||||
return await render_template(
|
||||
"invite_reset_success.html",
|
||||
@@ -235,6 +248,6 @@ async def reset_success() -> str:
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/-")
|
||||
@bp.route("/-") # type:ignore
|
||||
async def index() -> quart.Response:
|
||||
return redirect(url_for("index"))
|
||||
|
||||
@@ -15,23 +15,24 @@ from quart import (
|
||||
render_template,
|
||||
request,
|
||||
Response,
|
||||
flash,
|
||||
)
|
||||
import werkzeug.exceptions
|
||||
|
||||
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()],
|
||||
@@ -47,12 +48,15 @@ class LoginForm(flask_wtf.FlaskForm): # type:ignore
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/-")
|
||||
@bp.route("/-") # type:ignore
|
||||
async def index() -> quart.Response:
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
@bp.route("/login", methods=["GET", "POST"])
|
||||
ERR_CREDENTIALS_INVALID = _l("Invalid username or password.")
|
||||
|
||||
|
||||
@bp.route("/login", methods=["GET", "POST"]) # type:ignore
|
||||
async def login() -> typing.Union[str, quart.Response]:
|
||||
if client.has_session and (await client.test_session()):
|
||||
return redirect(url_for('user.index'))
|
||||
@@ -63,38 +67,53 @@ 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 werkzeug.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")
|
||||
@bp.route("/meta/about.html") # type:ignore
|
||||
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 werkzeug.exceptions.Unauthorized:
|
||||
extra_versions["Prosody"] = "unknown"
|
||||
|
||||
return await render_template(
|
||||
"about.html",
|
||||
version=_version.version,
|
||||
version=version,
|
||||
extra_versions=extra_versions,
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/meta/demo.html")
|
||||
@bp.route("/meta/demo.html") # type:ignore
|
||||
async def demo() -> str:
|
||||
return await render_template("demo.html")
|
||||
|
||||
@@ -103,8 +122,9 @@ def repad(s: str) -> str:
|
||||
return s + "=" * (4 - len(s) % 4)
|
||||
|
||||
|
||||
@bp.route("/avatar/<from_>/<code>")
|
||||
@bp.route("/avatar/<from_>/<code>") # type:ignore
|
||||
async def avatar(from_: str, code: str) -> quart.Response:
|
||||
etag: typing.Optional[str]
|
||||
try:
|
||||
etag = request.headers["if-none-match"]
|
||||
except KeyError:
|
||||
@@ -144,3 +164,8 @@ async def avatar(from_: str, code: str) -> quart.Response:
|
||||
|
||||
response.set_data(data)
|
||||
return response
|
||||
|
||||
|
||||
@bp.route("/_health") # type:ignore
|
||||
async def health() -> Response:
|
||||
return Response("STATUS OK", content_type="text/plain")
|
||||
|
||||
@@ -19,7 +19,8 @@ from quart import (
|
||||
current_app, _app_ctx_stack, session as http_session, abort, redirect,
|
||||
url_for,
|
||||
)
|
||||
import quart.exceptions
|
||||
import werkzeug.exceptions
|
||||
import quart
|
||||
|
||||
from . import xmpputil
|
||||
from .xmpputil import split_jid
|
||||
@@ -44,6 +45,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 +65,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"),
|
||||
)
|
||||
|
||||
|
||||
@@ -144,6 +155,7 @@ class HTTPSessionManager:
|
||||
async def _create(self) -> aiohttp.ClientSession:
|
||||
return aiohttp.ClientSession(headers={
|
||||
"Accept": "application/json",
|
||||
"Host": current_app.config["SNIKKET_DOMAIN"],
|
||||
})
|
||||
|
||||
async def teardown(self, exc: typing.Optional[BaseException]) -> None:
|
||||
@@ -204,6 +216,7 @@ class HTTPAuthSessionManager(HTTPSessionManager):
|
||||
headers={
|
||||
"Authorization": "Bearer {}".format(token),
|
||||
"Accept": "application/json",
|
||||
"Host": current_app.config["SNIKKET_DOMAIN"],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -259,8 +272,9 @@ class ProsodyClient:
|
||||
|
||||
def init_app(self, app: quart.Quart) -> None:
|
||||
app.config[self.CONFIG_ENDPOINT]
|
||||
app.teardown_appcontext(self._plain_session.teardown)
|
||||
app.teardown_appcontext(self._auth_session.teardown)
|
||||
# the type annotation in quart seems to be wrong here
|
||||
app.teardown_appcontext(self._plain_session.teardown) # type:ignore
|
||||
app.teardown_appcontext(self._auth_session.teardown) # type:ignore
|
||||
|
||||
@property
|
||||
def _endpoint_base(self) -> str:
|
||||
@@ -330,15 +344,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
|
||||
@@ -443,6 +460,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(
|
||||
@@ -467,7 +491,7 @@ class ProsodyClient:
|
||||
session=session,
|
||||
)
|
||||
avatar_hash = avatar_info["sha1"]
|
||||
except quart.exceptions.HTTPException:
|
||||
except werkzeug.exceptions.HTTPException:
|
||||
avatar_hash = None
|
||||
|
||||
return {
|
||||
@@ -488,9 +512,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,
|
||||
@@ -596,7 +643,7 @@ class ProsodyClient:
|
||||
new_access_model,
|
||||
)
|
||||
))
|
||||
except quart.exceptions.NotFound:
|
||||
except werkzeug.exceptions.NotFound:
|
||||
if ignore_not_found:
|
||||
return
|
||||
raise
|
||||
@@ -726,7 +773,7 @@ class ProsodyClient:
|
||||
session: aiohttp.ClientSession,
|
||||
) -> str:
|
||||
access_models = filter(
|
||||
lambda x: not isinstance(x, quart.exceptions.NotFound),
|
||||
lambda x: not isinstance(x, werkzeug.exceptions.NotFound),
|
||||
await asyncio.gather(
|
||||
self.get_avatar_access_model(session=session),
|
||||
self.get_nickname_access_model(session=session),
|
||||
@@ -765,7 +812,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,
|
||||
@@ -777,14 +824,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,
|
||||
@@ -823,6 +870,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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 15 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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
File diff suppressed because it is too large
Load Diff
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.
1452
snikket_web/translations/it/LC_MESSAGES/messages.po
Normal file
1452
snikket_web/translations/it/LC_MESSAGES/messages.po
Normal file
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 "このサークルの更新ができません"
|
||||
|
||||
|
||||
@@ -8,7 +8,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: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -17,124 +17,214 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.9.0\n"
|
||||
|
||||
#: snikket_web/admin.py:60
|
||||
msgid "Delete user permanently"
|
||||
#: snikket_web/admin.py:59
|
||||
msgid "Limited"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:129
|
||||
msgid "Invite to circle"
|
||||
#: 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:135
|
||||
msgid "At least one circle must be selected"
|
||||
#: 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:140
|
||||
msgid "Valid for"
|
||||
#: 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
|
||||
msgid "Administrator"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:83
|
||||
msgid "Update user"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:87
|
||||
msgid "Create password reset link"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:105
|
||||
msgid "Password reset link created"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:120
|
||||
msgid "User information updated."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:142
|
||||
msgid "Delete user permanently"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:155
|
||||
msgid "User deleted"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:193
|
||||
msgid "Password reset link not found"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:205
|
||||
msgid "Password reset link deleted"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:225
|
||||
msgid "Invite to circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:231
|
||||
msgid "At least one circle must be selected"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:236
|
||||
msgid "Valid for"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:238
|
||||
msgid "One hour"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:143
|
||||
#: snikket_web/admin.py:239
|
||||
msgid "Twelve hours"
|
||||
msgstr ""
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: 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
|
||||
msgid "Invitation created"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:359
|
||||
msgid "No such invitation exists"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:374
|
||||
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
|
||||
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
|
||||
msgid "No such circle exists"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:512
|
||||
msgid "Circle data updated"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:518
|
||||
msgid "Circle deleted"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:529
|
||||
msgid "User added to circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:538
|
||||
msgid "User removed from circle"
|
||||
msgstr ""
|
||||
|
||||
#: 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
|
||||
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
|
||||
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
|
||||
msgid "Registration was declined for unknown reasons."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/invite.py:145
|
||||
msgid "The username is not valid"
|
||||
#: snikket_web/invite.py:158
|
||||
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 ""
|
||||
|
||||
@@ -146,69 +236,92 @@ 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"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:42
|
||||
msgid "Sign out"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:47
|
||||
msgid "Nobody"
|
||||
#: snikket_web/user.py:41
|
||||
msgid "The new passwords must match."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:48
|
||||
msgid "Sign out"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:53
|
||||
msgid "Nobody"
|
||||
msgstr ""
|
||||
|
||||
#: 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
|
||||
msgid "Incorrect password."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/_footer.html:4 snikket_web/templates/login.html:36
|
||||
#: snikket_web/user.py:103
|
||||
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
|
||||
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 ""
|
||||
|
||||
@@ -257,10 +370,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 ""
|
||||
|
||||
@@ -296,7 +421,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 ""
|
||||
|
||||
@@ -350,7 +475,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 ""
|
||||
|
||||
@@ -367,12 +492,12 @@ msgid "Copy complete output"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:4
|
||||
#: snikket_web/templates/admin_users.html:22
|
||||
#, python-format
|
||||
msgid "Delete user %(user_name)s"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:6
|
||||
#: snikket_web/templates/admin_edit_user.html:54
|
||||
msgid "Delete user"
|
||||
msgstr ""
|
||||
|
||||
@@ -380,11 +505,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 ""
|
||||
@@ -397,12 +517,10 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:19
|
||||
#: snikket_web/templates/admin_edit_circle.html:44
|
||||
#: snikket_web/templates/admin_edit_invite.html:49
|
||||
#: snikket_web/templates/admin_reset_user_password.html:25
|
||||
#: snikket_web/templates/user_logout.html: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 ""
|
||||
|
||||
@@ -434,6 +552,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 ""
|
||||
@@ -476,7 +598,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 ""
|
||||
@@ -517,6 +638,85 @@ msgstr ""
|
||||
msgid "Created"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_invite.html:48
|
||||
msgid "Return to invitation list"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:5
|
||||
msgid ""
|
||||
"Limited users can interact with users on the same Snikket service and be "
|
||||
"members of circles."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:7
|
||||
msgid ""
|
||||
"Like limited users and can also interact with users on other Snikket "
|
||||
"services."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:9
|
||||
msgid "Like normal users and can access the admin panel in the web portal."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:20
|
||||
#: snikket_web/templates/admin_users.html:28
|
||||
#, python-format
|
||||
msgid "Edit user %(user_name)s"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:23
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
msgid "Show debug information"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_home.html:4
|
||||
msgid "Welcome to the admin panel!"
|
||||
msgstr ""
|
||||
@@ -568,6 +768,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 ""
|
||||
@@ -607,14 +811,20 @@ 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
|
||||
msgid "The user is an 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:19
|
||||
msgid " (Administrator)"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_users.html:22
|
||||
msgid "The user is restricted."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_users.html:22
|
||||
msgid " (Restricted)"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/app.html:4
|
||||
@@ -649,7 +859,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 ""
|
||||
@@ -657,7 +867,7 @@ msgstr ""
|
||||
#: 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 ""
|
||||
@@ -698,18 +908,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 ""
|
||||
|
||||
@@ -795,7 +1007,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 "
|
||||
@@ -807,7 +1019,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 ""
|
||||
|
||||
@@ -848,29 +1060,29 @@ msgstr ""
|
||||
msgid "Invite to %(site_name)s | Snikket"
|
||||
msgstr ""
|
||||
|
||||
#: 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 "
|
||||
@@ -878,25 +1090,30 @@ msgid ""
|
||||
"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 "
|
||||
@@ -904,42 +1121,82 @@ msgid ""
|
||||
"href=\"%(register_url)s\">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 ""
|
||||
@@ -968,8 +1225,19 @@ msgstr ""
|
||||
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
|
||||
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
|
||||
@@ -1005,46 +1273,46 @@ 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_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
|
||||
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."
|
||||
|
||||
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
1474
snikket_web/translations/ru/LC_MESSAGES/messages.po
Normal file
1474
snikket_web/translations/ru/LC_MESSAGES/messages.po
Normal file
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
@@ -1,22 +1,29 @@
|
||||
import asyncio
|
||||
import typing
|
||||
|
||||
import quart
|
||||
import quart.flask_patch
|
||||
from quart import Blueprint, render_template, request, redirect, url_for
|
||||
import quart.exceptions
|
||||
from quart import (
|
||||
Blueprint,
|
||||
render_template,
|
||||
request,
|
||||
redirect,
|
||||
url_for,
|
||||
flash,
|
||||
current_app,
|
||||
)
|
||||
import werkzeug.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 +39,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 +57,7 @@ _ACCESS_MODEL_CHOICES = [
|
||||
]
|
||||
|
||||
|
||||
class ProfileForm(flask_wtf.FlaskForm): # type:ignore
|
||||
class ProfileForm(BaseForm):
|
||||
nickname = wtforms.TextField(
|
||||
_l("Display name"),
|
||||
)
|
||||
@@ -69,14 +76,14 @@ class ProfileForm(flask_wtf.FlaskForm): # type:ignore
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/")
|
||||
@bp.route("/") # type:ignore
|
||||
@client.require_session()
|
||||
async def index() -> str:
|
||||
user_info = await client.get_user_info()
|
||||
return await render_template("user_home.html", user_info=user_info)
|
||||
|
||||
|
||||
@bp.route('/passwd', methods=["GET", "POST"])
|
||||
@bp.route('/passwd', methods=["GET", "POST"]) # type:ignore
|
||||
@client.require_session()
|
||||
async def change_pw() -> typing.Union[str, quart.Response]:
|
||||
form = ChangePasswordForm()
|
||||
@@ -86,21 +93,33 @@ async def change_pw() -> typing.Union[str, quart.Response]:
|
||||
form.current_password.data,
|
||||
form.new_password.data,
|
||||
)
|
||||
except (quart.exceptions.Unauthorized,
|
||||
quart.exceptions.Forbidden):
|
||||
except (werkzeug.exceptions.Unauthorized,
|
||||
werkzeug.exceptions.Forbidden):
|
||||
# server refused current password, set an appropriate error
|
||||
form.current_password.errors.append(
|
||||
_("Incorrect password"),
|
||||
_("Incorrect password."),
|
||||
)
|
||||
else:
|
||||
await flash(
|
||||
_("Password changed"),
|
||||
"success",
|
||||
)
|
||||
return redirect(url_for("user.change_pw"))
|
||||
|
||||
return await render_template("user_passwd.html", form=form)
|
||||
|
||||
|
||||
@bp.route("/profile", methods=["GET", "POST"])
|
||||
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"]) # type:ignore
|
||||
@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,34 +133,54 @@ 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"])
|
||||
@bp.route("/logout", methods=["GET", "POST"]) # type:ignore
|
||||
@client.require_session()
|
||||
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)
|
||||
|
||||
@@ -4,7 +4,7 @@ import typing
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from quart import abort
|
||||
import quart.exceptions
|
||||
import werkzeug.exceptions
|
||||
|
||||
|
||||
TAG_XMPP_ERROR = "error"
|
||||
@@ -234,7 +234,7 @@ def extract_pubsub_item_get_reply(
|
||||
) -> typing.Optional[ET.Element]:
|
||||
try:
|
||||
pubsub = extract_iq_reply(iq_tree, TAG_PUBSUB)
|
||||
except quart.exceptions.NotFound:
|
||||
except werkzeug.exceptions.NotFound:
|
||||
return None
|
||||
|
||||
if pubsub is None:
|
||||
|
||||
@@ -5,6 +5,7 @@ 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
|
||||
content/add_circle_outline:add
|
||||
|
||||
Reference in New Issue
Block a user