Implement group support (we call ’em circles)

This commit is contained in:
Jonas Schäfer
2021-01-18 17:36:43 +01:00
parent 006fea97a6
commit 17efe53106
20 changed files with 1165 additions and 233 deletions

View File

@@ -1,18 +1,28 @@
import asyncio
import typing import typing
from datetime import datetime from datetime import datetime
import aiohttp
import quart.flask_patch import quart.flask_patch
import wtforms import wtforms
import wtforms.fields.html5 import wtforms.fields.html5
from quart import (Blueprint, render_template, redirect, url_for) from quart import (
Blueprint,
render_template,
redirect,
url_for,
request,
)
import flask_wtf import flask_wtf
from flask_babel import lazy_gettext as _l from flask_babel import lazy_gettext as _l
from .infra import client from . import prosodyclient
from .infra import client, circle_name
bp = Blueprint("admin", __name__, url_prefix="/admin") bp = Blueprint("admin", __name__, url_prefix="/admin")
@@ -67,10 +77,55 @@ async def delete_user(localpart: str) -> typing.Union[str, quart.Response]:
class InvitesListForm(flask_wtf.FlaskForm): # type:ignore class InvitesListForm(flask_wtf.FlaskForm): # type:ignore
action_revoke = wtforms.StringField() action_revoke = wtforms.StringField()
class InvitePost(flask_wtf.FlaskForm): # type:ignore
circles = wtforms.SelectMultipleField(
_l("Invite to circle"),
# NOTE: This is for when/if we ever support multi-group invites.
# also see the note in admin_create_invite_form.html
# option_widget=wtforms.widgets.CheckboxInput(),
widget=wtforms.widgets.Select(multiple=False),
validators=[wtforms.validators.InputRequired(
_l("At least one circle must be selected")
)],
)
lifetime = wtforms.SelectField(
_l("Valid for"),
choices=[
(3600, _l("One hour")),
(12*3600, _l("Twelve hours")),
(86400, _l("One day")),
(7*86400, _l("One week")),
(28*86400, _l("Four weeks")),
],
default=7*86400,
)
reusable = wtforms.BooleanField(
_l("Allow multiple uses"),
)
action_create_invite = wtforms.SubmitField( action_create_invite = wtforms.SubmitField(
_l("New invitation link") _l("New invitation link")
) )
async def init_choices(
self,
*,
circles: typing.Optional[typing.Collection[
prosodyclient.AdminGroupInfo
]] = None) -> None:
if circles is not None:
self.circles.choices = [
(circle.id_, circle_name(circle))
for circle in sorted(circles, key=lambda x: x.name)
]
return
return await self.init_choices(
circles=await client.list_groups()
)
@bp.route("/invitations", methods=["GET", "POST"]) @bp.route("/invitations", methods=["GET", "POST"])
@client.require_admin_session() @client.require_admin_session()
@@ -78,23 +133,34 @@ async def invitations() -> typing.Union[str, quart.Response]:
user_info = await client.get_user_info() user_info = await client.get_user_info()
invites = sorted( invites = sorted(
await client.list_invites(), await client.list_invites(),
key=lambda x: x.created_at key=lambda x: x.created_at,
reverse=True,
) )
circles = sorted(
await client.list_groups(),
key=lambda x: x.name
)
circle_map = {
circle.id_: circle
for circle in circles
}
invite_form = InvitePost()
await invite_form.init_choices(circles=circles)
form = InvitesListForm() form = InvitesListForm()
if form.validate_on_submit(): if form.validate_on_submit():
if form.action_revoke.data: if form.action_revoke.data:
await client.delete_invite(form.action_revoke.data) await client.delete_invite(form.action_revoke.data)
if form.action_create_invite.data:
info = await client.create_invite()
return redirect(url_for(".edit_invite", id_=info.id_))
return redirect(url_for(".invitations")) return redirect(url_for(".invitations"))
return await render_template( return await render_template(
"admin_invites.html", "admin_invites.html",
user_info=user_info, user_info=user_info,
invites=invites, invites=invites,
invite_form=invite_form,
now=datetime.utcnow(), now=datetime.utcnow(),
circle_map=circle_map,
form=form, form=form,
) )
@@ -105,11 +171,37 @@ class InviteForm(flask_wtf.FlaskForm): # type:ignore
) )
@bp.route("/invitation/-/new", methods=["POST"])
@client.require_admin_session()
async def create_invite() -> typing.Union[str, quart.Response]:
user_info = await client.get_user_info()
form = InvitePost()
circles = await client.list_groups()
form.circles.choices = [
(c.id_, c.name) for c in circles
]
if form.validate_on_submit():
invite = await client.create_invite(
group_ids=form.circles.data,
reusable=form.reusable.data,
ttl=form.lifetime.data,
)
return redirect(url_for(".edit_invite", id_=invite.id_))
return await render_template("admin_create_invite.html",
user_info=user_info,
invite_form=form)
@bp.route("/invitation/<id_>", methods=["GET", "POST"]) @bp.route("/invitation/<id_>", methods=["GET", "POST"])
@client.require_admin_session() @client.require_admin_session()
async def edit_invite(id_: str) -> typing.Union[str, quart.Response]: async def edit_invite(id_: str) -> typing.Union[str, quart.Response]:
user_info = await client.get_user_info() user_info = await client.get_user_info()
invite_info = await client.get_invite_by_id(id_) invite_info = await client.get_invite_by_id(id_)
circles = await client.list_groups()
circle_map = {
circle.id_: circle
for circle in circles
}
form = InviteForm() form = InviteForm()
if form.validate_on_submit(): if form.validate_on_submit():
@@ -124,4 +216,120 @@ async def edit_invite(id_: str) -> typing.Union[str, quart.Response]:
invite=invite_info, invite=invite_info,
now=datetime.utcnow(), now=datetime.utcnow(),
form=form, form=form,
circle_map=circle_map,
)
class CirclePost(flask_wtf.FlaskForm): # type:ignore
name = wtforms.StringField(
_l("Name"),
)
action_create = wtforms.SubmitField(
_l("Create circle")
)
@bp.route("/circles")
@client.require_admin_session()
async def circles() -> str:
user_info = await client.get_user_info()
circles = sorted(
await client.list_groups(),
key=lambda x: x.name
)
invite_form = InvitePost()
create_form = CirclePost()
return await render_template(
"admin_circles.html",
circles=circles,
user_info=user_info,
invite_form=invite_form,
create_form=create_form,
)
@bp.route("/circle/-/new", methods=["POST"])
@client.require_admin_session()
async def create_circle() -> typing.Union[str, quart.Response]:
user_info = await client.get_user_info()
create_form = CirclePost()
if create_form.validate_on_submit():
circle = await client.create_group(
name=create_form.name.data,
)
return redirect(url_for(".edit_circle", id_=circle.id_))
return await render_template(
"admin_create_circle.html",
user_info=user_info,
create_form=create_form,
)
class EditCircleForm(flask_wtf.FlaskForm): # type:ignore
name = wtforms.StringField(
_l("Name"),
)
action_save = wtforms.SubmitField(
_l("Apply")
)
action_delete = wtforms.SubmitField(
_l("Delete circle permanently")
)
action_remove_user = wtforms.StringField()
@bp.route("/circle/<id_>", methods=["GET", "POST"])
@client.require_admin_session()
async def edit_circle(id_: str) -> typing.Union[str, quart.Response]:
async with client.authenticated_session() as session:
user_info = await client.get_user_info(
session=session,
)
try:
circle = await client.get_group_by_id(
id_,
session=session,
)
except aiohttp.ClientResponseError as exc:
if exc.status == 404:
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)
))
form = EditCircleForm()
invite_form = InvitePost()
await invite_form.init_choices()
invite_form.circles.data = [id_]
if request.method != "POST":
form.name.data = circle.name
if form.validate_on_submit():
if form.action_save.data:
# TODO: post update
pass
elif form.action_delete.data:
await client.delete_group(id_)
return redirect(url_for(".circles"))
return redirect(url_for(".edit_circle", id_=id_))
return await render_template(
"admin_edit_circle.html",
target_circle=circle,
user_info=user_info,
form=form,
circle_members=circle_members,
invite_form=invite_form,
) )

View File

@@ -8,6 +8,7 @@ from quart import (
) )
import flask_babel import flask_babel
from flask_babel import _
from . import prosodyclient from . import prosodyclient
@@ -20,9 +21,11 @@ babel = flask_babel.Babel()
@babel.localeselector # type:ignore @babel.localeselector # type:ignore
def selected_locale() -> str: def selected_locale() -> str:
return request.accept_languages.best_match( selected = request.accept_languages.best_match(
current_app.config['LANGUAGES'] current_app.config['LANGUAGES']
) )
print(request.accept_languages, current_app.config["LANGUAGES"], selected)
return selected
def flatten(a: typing.Iterable, levels: int = 1) -> typing.Iterable: def flatten(a: typing.Iterable, levels: int = 1) -> typing.Iterable:
@@ -31,6 +34,12 @@ def flatten(a: typing.Iterable, levels: int = 1) -> typing.Iterable:
return a return a
def circle_name(c: typing.Any) -> str:
if c.id_ == "default" and c.name == "default":
return _("Main")
return c.name
def init_templating(app: quart.Quart) -> None: def init_templating(app: quart.Quart) -> None:
app.template_filter("repr")(repr) app.template_filter("repr")(repr)
app.template_filter("format_datetime")(flask_babel.format_datetime) app.template_filter("format_datetime")(flask_babel.format_datetime)
@@ -38,3 +47,4 @@ def init_templating(app: quart.Quart) -> None:
app.template_filter("format_time")(flask_babel.format_time) app.template_filter("format_time")(flask_babel.format_time)
app.template_filter("format_timedelta")(flask_babel.format_timedelta) app.template_filter("format_timedelta")(flask_babel.format_timedelta)
app.template_filter("flatten")(flatten) app.template_filter("flatten")(flatten)
app.template_filter("circle_name")(circle_name)

View File

@@ -39,6 +39,10 @@ class LoginForm(flask_wtf.FlaskForm): # type:ignore
validators=[wtforms.validators.InputRequired()], validators=[wtforms.validators.InputRequired()],
) )
action_signin = wtforms.SubmitField(
_l("Sign in"),
)
@bp.route("/login", methods=["GET", "POST"]) @bp.route("/login", methods=["GET", "POST"])
async def login() -> typing.Union[str, quart.Response]: async def login() -> typing.Union[str, quart.Response]:

View File

@@ -72,6 +72,8 @@ class AdminInviteInfo:
landing_page: typing.Optional[str] landing_page: typing.Optional[str]
created_at: datetime created_at: datetime
expires: datetime expires: datetime
reusable: bool
group_ids: typing.Collection[str]
@classmethod @classmethod
def from_api_response( def from_api_response(
@@ -87,6 +89,26 @@ class AdminInviteInfo:
token=data["id"], token=data["id"],
xmpp_uri=data.get("xmpp_uri"), xmpp_uri=data.get("xmpp_uri"),
landing_page=data.get("landing_page"), landing_page=data.get("landing_page"),
group_ids=data.get("groups", []),
reusable=data["reusable"],
)
@dataclasses.dataclass(frozen=True)
class AdminGroupInfo:
id_: str
name: str
members: typing.Collection[str]
@classmethod
def from_api_response(
cls,
data: typing.Mapping[str, typing.Any],
) -> "AdminGroupInfo":
return cls(
id_=data["id"],
name=data["name"],
members=data["members"],
) )
@@ -305,6 +327,9 @@ class ProsodyClient:
def has_session(self) -> bool: def has_session(self) -> bool:
return self.SESSION_TOKEN in http_session return self.SESSION_TOKEN in http_session
def authenticated_session(self) -> HTTPAuthSessionManager:
return self._auth_session
def require_session( def require_session(
self, self,
redirect_to: typing.Optional[str] = None, redirect_to: typing.Optional[str] = None,
@@ -394,29 +419,33 @@ class ProsodyClient:
) )
return ET.fromstring(reply_payload) return ET.fromstring(reply_payload)
async def get_user_info(self) -> typing.Mapping: @autosession
async def get_user_info(
self,
*,
session: aiohttp.ClientSession,
) -> typing.Mapping:
localpart, domain, _ = split_jid(self.session_address) localpart, domain, _ = split_jid(self.session_address)
async with self._auth_session as session: nickname = await self.get_user_nickname(session=session)
nickname = await self.get_user_nickname(session=session) try:
try: avatar_info = await self.get_avatar(
avatar_info = await self.get_avatar( self.session_address,
self.session_address, metadata_only=True,
metadata_only=True, session=session,
session=session, )
) avatar_hash = avatar_info["sha1"]
avatar_hash = avatar_info["sha1"] except quart.exceptions.HTTPException:
except quart.exceptions.HTTPException: avatar_hash = None
avatar_hash = None
return { return {
"address": self.session_address, "address": self.session_address,
"username": localpart, "username": localpart,
"nickname": nickname, "nickname": nickname,
"display_name": nickname or localpart, "display_name": nickname or localpart,
"avatar_hash": avatar_hash, "avatar_hash": avatar_hash,
"is_admin": self.is_admin_session, "is_admin": self.is_admin_session,
} }
@autosession @autosession
async def test_session(self, session: aiohttp.ClientSession) -> bool: async def test_session(self, session: aiohttp.ClientSession) -> bool:
@@ -698,7 +727,7 @@ class ProsodyClient:
if resp.status == 400: if resp.status == 400:
abort(500, "request rejected by backend") abort(500, "request rejected by backend")
if not 200 <= resp.status < 300: if not 200 <= resp.status < 300:
abort(resp.status) resp.raise_for_status()
@autosession @autosession
async def list_users( async def list_users(
@@ -777,13 +806,89 @@ class ProsodyClient:
@autosession @autosession
async def create_invite( async def create_invite(
self, self,
group_ids: typing.Collection[str],
reusable: bool,
ttl: int,
*, *,
session: aiohttp.ClientSession, session: aiohttp.ClientSession,
) -> AdminInviteInfo: ) -> AdminInviteInfo:
async with session.put(self._admin_v1_endpoint("/invites")) as resp: payload = {
"reusable": reusable,
"groups": list(group_ids),
"ttl": ttl,
}
async with session.post(
self._admin_v1_endpoint("/invites"),
json=payload) as resp:
self._raise_error_from_response(resp) self._raise_error_from_response(resp)
return AdminInviteInfo.from_api_response(await resp.json()) return AdminInviteInfo.from_api_response(await resp.json())
@autosession
async def create_group(
self,
name: str,
*,
session: aiohttp.ClientSession,
) -> AdminGroupInfo:
payload = {
"name": name,
}
async with session.post(
self._admin_v1_endpoint("/groups"),
json=payload) as resp:
self._raise_error_from_response(resp)
return AdminGroupInfo.from_api_response(await resp.json())
@autosession
async def list_groups(
self,
*,
session: aiohttp.ClientSession,
) -> typing.Collection[AdminGroupInfo]:
async with session.get(self._admin_v1_endpoint("/groups")) as resp:
self._raise_error_from_response(resp)
return list(map(
AdminGroupInfo.from_api_response,
await resp.json(),
))
@autosession
async def get_group_by_id(
self,
id_: str,
*,
session: aiohttp.ClientSession,
) -> AdminGroupInfo:
async with session.get(
self._admin_v1_endpoint("/groups/{}".format(id_)),
) as resp:
self._raise_error_from_response(resp)
return AdminGroupInfo.from_api_response(await resp.json())
@autosession
async def update_group(
self,
id_: str,
*,
new_name: typing.Optional[str] = None,
session: aiohttp.ClientSession,
) -> AdminGroupInfo:
pass
@autosession
async def delete_group(
self,
id_: str,
*,
session: aiohttp.ClientSession,
) -> None:
async with session.delete(
self._admin_v1_endpoint("/groups/{}".format(id_)),
) as resp:
self._raise_error_from_response(resp)
async def logout(self) -> None: async def logout(self) -> None:
# this currently only kills the cookie stuff, we may want to invalidate # this currently only kills the cookie stuff, we may want to invalidate
# the token on the server side, toos # the token on the server side, toos

View File

@@ -250,3 +250,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%]; $h-small-sizes: [150.0%, 138.31618672%, 127.54245006%, 117.60790225%, 108.44717712%, 100.0%];
$small-screen-threshold: 40rem; $small-screen-threshold: 40rem;
$medium-screen-threshold: 60rem;

View File

@@ -805,6 +805,22 @@ table {
} }
} }
div.elevated {
margin: $w-l1;
padding: $w-l1;
background-color: white;
}
div.elevated > *:first-child {
margin-top: 0;
margin-bottom: 0;
}
div.elevated > *:last-child {
margin-top: 0;
margin-bottom: 0;
}
.long-url-link { .long-url-link {
display: block; display: block;
overflow: hidden; overflow: hidden;
@@ -820,15 +836,47 @@ table {
display: none; display: none;
} }
ul.inline {
display: inline;
margin: 0;
padding: 0;
list-style-type: none;
> li {
display: inline-block;
padding: 0;
margin: 0;
}
> li:before {
content: ', ';
}
> li:first-child:before {
content: '';
}
}
.nowrap {
white-space: nowrap;
}
/* linearisation / responsive stuff */ /* linearisation / responsive stuff */
@media screen and (max-width: $small-screen-threshold) { @media screen and (max-width: $medium-screen-threshold) {
main > .form.layout-expanded { .form.layout-expanded {
margin-left: 0; margin-left: 0;
margin-right: 0; margin-right: 0;
} }
div.elevated {
margin-left: 0;
margin-right: 0;
}
}
@media screen and (max-width: $small-screen-threshold) {
.form.layout-expanded .box { .form.layout-expanded .box {
margin-left: 0; margin-left: 0;
margin-right: 0; margin-right: 0;

View File

@@ -0,0 +1,51 @@
{% extends "admin_app.html" %}
{% from "library.j2" import action_button, custom_form_button, form_button, circle_name %}
{% block content %}
<h1>{% trans %}Manage circles{% endtrans %}</h1>
{%- if circles -%}
<form method="POST" action="{{ url_for(".create_invite") }}">
{{- invite_form.csrf_token -}}
<div class="elevated el-2"><table>
<thead>
<tr>
<th>{% trans %}Circle name{% endtrans %}</th>
<th class="collapsible">{% trans %}Members{% endtrans %}</th>
<th>{% trans %}Actions{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for circle in circles %}
<tr>
<td>{{ circle | circle_name }}</td>
<td class="collapsible">{{ circle.members | length }}</td>
<td class="nowrap">
{%- call custom_form_button("create_link", invite_form.circles.name, circle.id_, slim=True, class="secondary accent") -%}
{% trans circle_name=circle.name %}Create invitation to circle {{ circle_name }}{% endtrans %}
{%- endcall -%}
{%- call action_button("more", url_for(".edit_circle", id_=circle.id_), class="primary") -%}
{% trans circle_name=circle.name %}Show details of circle {{ circle_name }}{% endtrans %}
{%- endcall -%}
</td>
</tr>
{% endfor %}
</tbody>
</table></div></form>
{%- else -%}
<div class="box primary">
<header>{% trans %}No circles{% endtrans %}</header>
<p>{% trans %}Currently, there are no circles on this instance. Use the form below to create one.{% endtrans %}</p>
</div>
{%- endif -%}
<h2>{% trans %}New circle{% endtrans %}</h2>
<form method="POST" action="{{ url_for(".create_circle") }}"><div class="form layout-expanded">
{{- create_form.csrf_token -}}
<h2 class="form-title">{% trans %}Create circle{% endtrans %}</h2>
<div class="f-ebox">
{{- create_form.name.label -}}
{{- create_form.name -}}
</div>
<div class="f-bbox">
{%- call form_button("create_group", create_form.action_create, class="primary") -%}{%- endcall -%}
</div>
</div></form>
{% endblock %}

View File

@@ -0,0 +1,5 @@
{% extends "admin_app.html" %}
{% block content %}
<h1>{% trans %}Create invitation{% endtrans %}</h1>
{%- include "admin_create_invite_form.html" -%}
{% endblock %}

View File

@@ -0,0 +1,31 @@
{% from "library.j2" import form_button, render_errors %}
<form method="POST" action="{{ url_for(".create_invite") }}">
{{- invite_form.csrf_token -}}
<div class="form layout-expanded">
<h2 class="form-title">{% trans %}Create new invitation{% endtrans %}</h2>
<p class="form-descr weak">{% trans %}Create a new invitation link to invite more users to your Snikket instance by clicking the button below.{% endtrans %}</p>
<div class="f-ebox">
{{ invite_form.reusable }}
{{ invite_form.reusable.label }}
</div>
<div class="f-ebox">
{{ invite_form.lifetime.label }}
<div class="select-wrap">{{ invite_form.lifetime }}</div>
</div>
<div class="f-ebox">
{#
NOTE: This is for when/if we ever support multi-group invites.
Also see the NOTE in admin.py
{{ invite_form.circles.label(class="required") }}
{%- for choice in invite_form.circles -%}
{{ choice }}{{ choice.label }}
{%- endfor -%}
#}
{{- invite_form.circles.label -}}
<div class="select-wrap">{{ invite_form.circles }}</div>
{%- call render_errors(invite_form.circles) -%}{%- endcall -%}
</div>
<div class="f-bbox">
{%- call form_button("create_link", invite_form.action_create_invite, class="primary") %}{% endcall -%}
</div>
</div></form>

View File

@@ -0,0 +1,48 @@
{% extends "admin_app.html" %}
{% from "library.j2" import form_button, standard_button, value_or_hint, custom_form_button %}
{% block content %}
<h1>{% trans circle_name=(target_circle | circle_name) %}Edit circle {{ circle_name }}{% endtrans %}</h1>
<div class="form layout-expanded"><form method="POST">
<h2 class="form-title">{% trans %}Circle information{% endtrans %}</h2>
{{ form.csrf_token }}
<div class="f-ebox">
{{ form.name.label }}
{{ form.name }}
</div>
<div class="f-bbox">
{%- call standard_button("back", url_for(".circles"), class="secondary") -%}
{% trans %}Back{% endtrans %}
{%- endcall -%}
{%- call form_button("done", form.action_save, class="primary") %}{% endcall -%}
</div>
<h3 class="form-title">{% trans %}Delete circle{% endtrans %}</h3>
<p class="form-desc">{% trans %}Deleting a circle does not delete any users in the circle.{% endtrans %}</p>
<div class="f-bbox">
{%- call form_button("done", form.action_delete, class="secondary danger") %}{% endcall -%}
</div>
</div>
<h2>{% trans %}Circle members{% endtrans %}</h2>
<div class="el-2 elevated"><table>
<thead>
<th>Login name</th>
<th class="collapsible">Display name</th>
<th>Actions</th>
</thead>
<tbody>
{%- for member in circle_members -%}
<tr>
<td>{{ member.localpart }}</td>
<td class="collapsible">{% call value_or_hint(member.display_name) %}{% endcall %}</td>
<td class="nowrap">
{%- call custom_form_button("remove", form.action_remove_user.name, member.localpart, class="primary danger", slim=True) -%}
{% trans username=member.localpart %}Remove user {{ username }} from circle{% endtrans %}
{%- endcall -%}
</td>
</tr>
{%- endfor -%}
</tbody>
</table></div>
</form>
<h3>{% trans %}Invite more members{% endtrans %}</h3>
{%- include "admin_create_invite_form.html" -%}
{% endblock %}

View File

@@ -10,12 +10,34 @@
{{ form.csrf_token }} {{ form.csrf_token }}
<div class="form layout-expanded"> <div class="form layout-expanded">
<dl> <dl>
<dt>{% trans %}Created{% endtrans %}</dt>
<dd>{{ invite.created_at | format_date }}</dd>
<dt>{% trans %}Valid until{% endtrans %}</dt> <dt>{% trans %}Valid until{% endtrans %}</dt>
<dd>{{ invite.expires | format_date }}</dd> <dd>{{ invite.expires | format_date }}</dd>
<dt>{% trans %}Link{% endtrans %}</dt> <dt>{% trans %}Link{% endtrans %}</dt>
<dd>{% call showuri(invite.landing_page) %}{% endcall %}</dd> <dd>{% call showuri(invite.landing_page) %}{% endcall %}</dd>
<dt>{% trans %}Reusability{% endtrans %}</dt>
<dd>{% if invite.reusable %}{% trans %}This invitation link can be used arbitrarily often, until it expires, is revoked or a server-wide user limit is reached.{% endtrans %}{% else %}{% trans %}This invitation link can only be used once and is then depleted.{% endtrans %}{% endif %}</dd>
{%- set ngroups = invite.group_ids | length -%}
{%- if ngroups > 1 -%}
{#- not supported via the web UI, but we should still display it properly -#}
<dt>{% trans %}Circles{% endtrans %}</dt>
<dd><p>{% trans %}Users joining via this invitation will be added to the following circles:{% endtrans %}</p><ul>
{%- for group_id in invite.group_ids -%}
<li>{{ circle_map[group_id] | circle_name }}</li>
{%- endfor -%}
</ul></dd>
{%- else -%}
<dt>{% trans %}Circle{% endtrans %}</dt>
<dd>
{%- if ngroups == 1 -%}
{%- set group_id = invite.group_ids[0] -%}
<a href="{{ url_for(".edit_circle", id_=group_id) }}">{{ circle_map[invite.group_ids[0]] | circle_name }}</a>
{%- else -%}
<em>{% trans %}The user will not be added to any circle and will have no contacts.{% endtrans %}</em>
{%- endif -%}
</dd>
{%- endif -%}
<dt>{% trans %}Created{% endtrans %}</dt>
<dd>{{ invite.created_at | format_date }}</dd>
</dl> </dl>
<div class="f-bbox"> <div class="f-bbox">
{%- call form_button("remove_link", form.action_revoke, class="secondary danger") %}{% endcall -%} {%- call form_button("remove_link", form.action_revoke, class="secondary danger") %}{% endcall -%}

View File

@@ -7,6 +7,9 @@
<h2>{% trans %}Manage users{% endtrans %}</h2> <h2>{% trans %}Manage users{% endtrans %}</h2>
<p>{% trans %}Modify administrative user information or delete users.{% endtrans %}</p> <p>{% trans %}Modify administrative user information or delete users.{% endtrans %}</p>
</a> </a>
<a class="card" href="{{ url_for('.circles') }}">
<h2>{% trans %}Manage circles{% endtrans %}</h2>
</a>
<a class="card" href="{{ url_for('.invitations') }}"> <a class="card" href="{{ url_for('.invitations') }}">
<h2>{% trans %}Manage invitations{% endtrans %}</h2> <h2>{% trans %}Manage invitations{% endtrans %}</h2>
<p>{% trans %}Create, revoke or view invitations.{% endtrans %}</p> <p>{% trans %}Create, revoke or view invitations.{% endtrans %}</p>

View File

@@ -6,33 +6,40 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h1>{% trans %}Manage invitations{% endtrans %}</h1> <h1>{% trans %}Manage invitations{% endtrans %}</h1>
<form method="POST">{{ form.csrf_token }} {%- include "admin_create_invite_form.html" -%}
<div class="form layout-expanded">
<h2 class="form-title">{% trans %}Create new invitation{% endtrans %}</h2>
<p class="form-descr weak">{% trans %}Create a new invitation link to invite more users to your Snikket instance by clicking the button below.{% endtrans %}</p>
<div class="f-bbox">
{%- call form_button("create_link", form.action_create_invite, class="primary") %}{% endcall -%}
</div>
</div>
<h2>{% trans %}Pending invitations{% endtrans %}</h2> <h2>{% trans %}Pending invitations{% endtrans %}</h2>
{% if invites %} {% if invites %}
<table> <form method="POST">
<col/> {{- form.csrf_token -}}
<div class="elevated el-2"><table>
<col/> <col/>
<col class="collapsible"/>
<col class="collapsible"/>
<col/> <col/>
<thead> <thead>
<tr> <tr>
<th>{% trans %}Created{% endtrans %}</th>
<th>{% trans %}Valid until{% endtrans %}</th> <th>{% trans %}Valid until{% endtrans %}</th>
<th class="collapsible">{% trans %}Reusable{% endtrans %}</th>
<th class="collapsible">{% trans %}Circle{% endtrans %}</th>
<th>{% trans %}Actions{% endtrans %}</th> <th>{% trans %}Actions{% endtrans %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for invite in invites %} {% for invite in invites %}
<tr> <tr>
<td>{{ invite.created_at | format_date }}</td>
<td>{{ (invite.expires - now) | format_timedelta(add_direction=True) }}</td> <td>{{ (invite.expires - now) | format_timedelta(add_direction=True) }}</td>
<td style="white-space: nowrap;"> <td class="collapsible">{% if invite.reusable %}{% trans %}Yes{% endtrans %}{% else %}{% trans
%}No{% endtrans %}{% endif %}</td>
<td class="collapsible">
{#- -#}
<ul class="inline">
{%- for group_id in invite.group_ids -%}
<li>{{ circle_map[group_id] | circle_name }}</li>
{%- endfor -%}
</ul>
{#- -#}
</td>
<td class="nowrap">
{%- call action_button("more", url_for(".edit_invite", id_=invite.id_), class="secondary") -%} {%- call action_button("more", url_for(".edit_invite", id_=invite.id_), class="secondary") -%}
{% trans %}Show invite details{% endtrans %} {% trans %}Show invite details{% endtrans %}
{%- endcall -%} {%- endcall -%}
@@ -46,9 +53,8 @@
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table></form></div>
{% else %} {% else %}
<p>{% trans %}Currently, there are no pending invitations.{% endtrans %}</p> <p>{% trans %}Currently, there are no pending invitations.{% endtrans %}</p>
{% endif %} {% endif %}
</form>
{% endblock %} {% endblock %}

View File

@@ -1,15 +1,8 @@
{% extends "admin_app.html" %} {% extends "admin_app.html" %}
{% from "library.j2" import icon %} {% from "library.j2" import action_button, value_or_hint %}
{% macro value_or_hint(v, caller=None) %}
{%- if v is not none -%}
{{- v -}}
{%- else -%}
{%- endif -%}
{% endmacro %}
{% block content %} {% block content %}
<h1>{% trans %}Manage users{% endtrans %}</h1> <h1>{% trans %}Manage users{% endtrans %}</h1>
<table> <div class="elevated el-2"><table>
<thead> <thead>
<tr> <tr>
<th>{% trans %}Login name{% endtrans %}</th> <th>{% trans %}Login name{% endtrans %}</th>
@@ -27,11 +20,12 @@
<td class="collapsible">{% call value_or_hint(user.email) %}{% endcall %}</td> <td class="collapsible">{% call value_or_hint(user.email) %}{% endcall %}</td>
<td class="collapsible">{% call value_or_hint(user.phone) %}{% endcall %}</td> <td class="collapsible">{% call value_or_hint(user.phone) %}{% endcall %}</td>
<td> <td>
{#- -#}<a class="button secondary btn-delete" href="{{ url_for(".delete_user", localpart=user.localpart) }}">{% call icon("remove") %}{% trans user_name=user.localpart %}Delete user {{ user_name }}{% endtrans %}{% endcall %}</a> {%- call action_button("remove", url_for(".delete_user", localpart=user.localpart), class="secondary") -%}
{#- -#} {% trans user_name=user.localpart %}Delete user {{ user_name }}{% endtrans %}
{%- endcall -%}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table></div>
{% endblock %} {% endblock %}

View File

@@ -72,3 +72,28 @@
{% endif -%} {% endif -%}
</a> </a>
{%- endmacro %} {%- endmacro %}
{% macro render_errors(field, caller=None) -%}
{%- if field.errors -%}
<div class="box warning">{#- -#}
<header>{% trans %}Invalid input{% endtrans %}</header>
{%- if field.errors | length == 1 -%}
<p>{{ field.errors[0] }}.</p>
{%- else -%}
<ul>
{%- for error in field.errors -%}
<li>{{ error }}</li>
{%- endfor -%}
</ul>
{%- endif -%}
</div>
{%- endif -%}
{%- endmacro %}
{% macro value_or_hint(v, caller=None) %}
{%- if v is not none -%}
{{- v -}}
{%- else -%}
{%- endif -%}
{% endmacro %}

View File

@@ -1,5 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% from "library.j2" import box, icon %} {% from "library.j2" import box, form_button %}
{% set body_id = "login" %} {% set body_id = "login" %}
{% block head_lead %} {% block head_lead %}
<title>{{ _("Snikket Login") }}</title> <title>{{ _("Snikket Login") }}</title>
@@ -28,7 +28,7 @@
{{ form.password(placeholder=form.password.label.text) }} {{ form.password(placeholder=form.password.label.text) }}
</div> </div>
<div class="f-bbox"> <div class="f-bbox">
<button type="submit" class="primary">{% call icon("login") %}{% endcall %}{{ _("Log in") }}</button> {%- call form_button("login", form.action_signin, class="primary") -%}{% endcall -%}
</div> </div>
</from> </from>
</div></main></div> </div></main></div>

View File

@@ -1,5 +1,5 @@
{% extends "app.html" %} {% extends "app.html" %}
{% from "library.j2" import icon %} {% from "library.j2" import standard_button, form_button %}
{% block head_lead %} {% block head_lead %}
<title>Snikket Web Portal</title> <title>Snikket Web Portal</title>
{% endblock %} {% endblock %}
@@ -9,11 +9,10 @@
<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> <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 }} {{ form.csrf_token }}
<div class="f-bbox"> <div class="f-bbox">
{#- -#} {%- call standard_button("back", url_for("user.index"), class="secondary") -%}
<a href="{{ url_for('user.index') }}" class="button secondary">{% call icon("back") %}{% endcall %}{% trans %}Back{% endtrans %}</a> {% trans %}Back{% endtrans %}
{#- -#} {%- endcall -%}
<button type="submit" class="primary">{% call icon("logout") %}{% endcall %}{% trans %}Sign out{% endtrans %}</button> {%- call form_button("logout", form.action_signout, class="primary") %}{% endcall -%}
{#- -#}
</div> </div>
</form></div> </form></div>
{% endblock %} {% endblock %}

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: SnikketWeb 0.1.0\n" "Project-Id-Version: SnikketWeb 0.1.0\n"
"Report-Msgid-Bugs-To: jonas@zombofant.net\n" "Report-Msgid-Bugs-To: jonas@zombofant.net\n"
"POT-Creation-Date: 2021-01-17 20:13+0100\n" "POT-Creation-Date: 2021-01-21 16:54+0100\n"
"PO-Revision-Date: 2020-03-07 16:32+0100\n" "PO-Revision-Date: 2020-03-07 16:32+0100\n"
"Last-Translator: Jonas Schäfer <jonas@zombofant.net>\n" "Last-Translator: Jonas Schäfer <jonas@zombofant.net>\n"
"Language: de\n" "Language: de\n"
@@ -18,18 +18,74 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.9.0\n" "Generated-By: Babel 2.9.0\n"
#: snikket_web/admin.py:44 #: snikket_web/admin.py:54
msgid "Delete user permanently" msgid "Delete user permanently"
msgstr "Benutzer endgültig löschen" msgstr "Benutzer endgültig löschen"
#: snikket_web/admin.py:71 #: snikket_web/admin.py:83
msgid "Invite to circle"
msgstr "In Gemeinschaft einladen"
#: snikket_web/admin.py:89
msgid "At least one circle must be selected"
msgstr "Mindestens eine Gemeinschaft muss ausgewählt sein"
#: snikket_web/admin.py:94
msgid "Valid for"
msgstr "Gültig für"
#: snikket_web/admin.py:96
msgid "One hour"
msgstr "Eine Stunde"
#: snikket_web/admin.py:97
msgid "Twelve hours"
msgstr "Zwölf Stunden"
#: snikket_web/admin.py:98
msgid "One day"
msgstr "Ein Tag"
#: snikket_web/admin.py:99
msgid "One week"
msgstr "Eine Woche"
#: snikket_web/admin.py:100
msgid "Four weeks"
msgstr "Vier Wochen"
#: snikket_web/admin.py:106
msgid "Allow multiple uses"
msgstr "Mehrfach verwendbar"
#: snikket_web/admin.py:110
msgid "New invitation link" msgid "New invitation link"
msgstr "Neuer Einladungslink" msgstr "Neuer Einladungslink"
#: snikket_web/admin.py:104 #: snikket_web/admin.py:170
msgid "Revoke" msgid "Revoke"
msgstr "Löschen" msgstr "Löschen"
#: snikket_web/admin.py:225 snikket_web/admin.py:272
msgid "Name"
msgstr "Name"
#: snikket_web/admin.py:229 snikket_web/templates/admin_circles.html:42
msgid "Create circle"
msgstr "Gemeinschaft gründen"
#: snikket_web/admin.py:276 snikket_web/user.py:73
msgid "Apply"
msgstr "Übernehmen"
#: snikket_web/admin.py:280
msgid "Delete circle permanently"
msgstr "Gemeinschaft endgültig löschen"
#: snikket_web/infra.py:39
msgid "Main"
msgstr "Kern"
#: snikket_web/main.py:33 #: snikket_web/main.py:33
msgid "Address" msgid "Address"
msgstr "Adresse" msgstr "Adresse"
@@ -38,7 +94,11 @@ msgstr "Adresse"
msgid "Password" msgid "Password"
msgstr "Passwort" msgstr "Passwort"
#: snikket_web/main.py:60 #: snikket_web/main.py:43
msgid "Sign in"
msgstr "Anmelden"
#: snikket_web/main.py:64
msgid "Invalid user name or password." msgid "Invalid user name or password."
msgstr "Benutzername oder Passwort falsch." msgstr "Benutzername oder Passwort falsch."
@@ -58,38 +118,103 @@ msgstr "Neues Passwort (Bestätigung)"
msgid "The new passwords must match." msgid "The new passwords must match."
msgstr "Die neuen Passwörter müssen übereinstimmen." msgstr "Die neuen Passwörter müssen übereinstimmen."
#: snikket_web/user.py:50 #: snikket_web/user.py:47
msgid "Sign out"
msgstr "Abmelden"
#: snikket_web/user.py:52
msgid "Nobody" msgid "Nobody"
msgstr "Niemand" msgstr "Niemand"
#: snikket_web/user.py:51 #: snikket_web/user.py:53
msgid "Friends only" msgid "Friends only"
msgstr "Nur Freunde" msgstr "Nur Freunde"
#: snikket_web/user.py:52 #: snikket_web/user.py:54
msgid "Everyone" msgid "Everyone"
msgstr "Jeder" msgstr "Jeder"
#: snikket_web/templates/admin_delete_user.html:12 #: snikket_web/templates/admin_delete_user.html:12
#: snikket_web/templates/admin_delete_user.html:16 #: snikket_web/templates/admin_delete_user.html:16
#: snikket_web/templates/admin_users.html:15 snikket_web/user.py:58 #: snikket_web/templates/admin_users.html:9 snikket_web/user.py:60
msgid "Display name" msgid "Display name"
msgstr "Anzeigename" msgstr "Anzeigename"
#: snikket_web/user.py:62 #: snikket_web/user.py:64
msgid "Avatar" msgid "Avatar"
msgstr "Bild" msgstr "Bild"
#: snikket_web/user.py:66 #: snikket_web/user.py:68
msgid "Profile visibility" msgid "Profile visibility"
msgstr "Profilsichtbarkeit" msgstr "Profilsichtbarkeit"
#: snikket_web/user.py:91 #: snikket_web/user.py:97
msgid "Incorrect password" msgid "Incorrect password"
msgstr "Ungültiges Passwort" msgstr "Ungültiges Passwort"
#: snikket_web/templates/admin_circles.html:4
#: snikket_web/templates/admin_home.html:11
msgid "Manage circles"
msgstr "Gemeinschaften verwalten"
#: snikket_web/templates/admin_circles.html:11
msgid "Circle name"
msgstr "Name"
#: snikket_web/templates/admin_circles.html:12
msgid "Members"
msgstr "Mitglieder"
#: snikket_web/templates/admin_circles.html:13
#: snikket_web/templates/admin_invites.html:24
#: snikket_web/templates/admin_users.html:12
msgid "Actions"
msgstr "Aktionen"
#: snikket_web/templates/admin_circles.html:23
#, python-format
msgid "Create invitation to circle %(circle_name)s"
msgstr "Einladung in die %(circle_name)s Gemeinschaft erzeugen"
#: snikket_web/templates/admin_circles.html:26
#, python-format
msgid "Show details of circle %(circle_name)s"
msgstr "Details der %(circle_name)s Gemeinschaft anzeigen"
#: snikket_web/templates/admin_circles.html:35
msgid "No circles"
msgstr "Keine Gemeinschaften"
#: snikket_web/templates/admin_circles.html:36
msgid ""
"Currently, there are no circles on this instance. Use the form below to "
"create one."
msgstr ""
"Es gibt derzeit keine Gemeinschaften auf dieser Instanz. Unten kannst du "
"eine anlegen."
#: snikket_web/templates/admin_circles.html:39
msgid "New circle"
msgstr "Neue Gemeinschaft"
#: snikket_web/templates/admin_create_invite.html:3
msgid "Create invitation"
msgstr "Gemeinschaft gründen"
#: snikket_web/templates/admin_create_invite_form.html:5
msgid "Create new invitation"
msgstr "Neue Einladung erzeugen"
#: snikket_web/templates/admin_create_invite_form.html:6
msgid ""
"Create a new invitation link to invite more users to your Snikket "
"instance by clicking the button below."
msgstr ""
"Erzeuge eine neue Einladung um mehr Benutzer auf deine Snikket-Instanz "
"einzuladen indem du den folgenden Button klickst."
#: snikket_web/templates/admin_delete_user.html:4 #: snikket_web/templates/admin_delete_user.html:4
#: snikket_web/templates/admin_users.html:29 #: snikket_web/templates/admin_users.html:24
#, python-format #, python-format
msgid "Delete user %(user_name)s" msgid "Delete user %(user_name)s"
msgstr "Benutzer %(user_name)s löschen" msgstr "Benutzer %(user_name)s löschen"
@@ -104,12 +229,12 @@ msgid "Are you sure you want to delete the following user?"
msgstr "Bist du sicher dass du den folgenden Benutzer löschen willst?" msgstr "Bist du sicher dass du den folgenden Benutzer löschen willst?"
#: snikket_web/templates/admin_delete_user.html:10 #: snikket_web/templates/admin_delete_user.html:10
#: snikket_web/templates/admin_users.html:14 #: snikket_web/templates/admin_users.html:8
msgid "Login name" msgid "Login name"
msgstr "Anmeldename" msgstr "Anmeldename"
#: snikket_web/templates/admin_delete_user.html:14 #: snikket_web/templates/admin_delete_user.html:14
#: snikket_web/templates/admin_users.html:16 #: snikket_web/templates/admin_users.html:10
msgid "Email address" msgid "Email address"
msgstr "E-Mail-Adresse" msgstr "E-Mail-Adresse"
@@ -117,30 +242,100 @@ msgstr "E-Mail-Adresse"
msgid "Danger" msgid "Danger"
msgstr "Gefahr" msgstr "Gefahr"
#: snikket_web/templates/admin_delete_user.html:23
#: snikket_web/templates/admin_edit_circle.html:14
#: snikket_web/templates/admin_edit_invite.html:45
#: snikket_web/templates/user_logout.html:13
#: snikket_web/templates/user_passwd.html:40
#: snikket_web/templates/user_profile.html:25
msgid "Back"
msgstr "Zurück"
#: snikket_web/templates/admin_edit_circle.html:4
#, python-format
msgid "Edit circle %(circle_name)s"
msgstr "Gemeinschaft %(circle_name)s bearbeiten"
#: snikket_web/templates/admin_edit_circle.html:6
msgid "Circle information"
msgstr "Gemeinschaftsinformationen"
#: snikket_web/templates/admin_edit_circle.html:18
msgid "Delete circle"
msgstr "Gemeinschaft löschen"
#: snikket_web/templates/admin_edit_circle.html:19
msgid "Deleting a circle does not delete any users in the circle."
msgstr ""
"Wenn eine Gemeinschaft gelöscht wird, werden die Benutzer die zu dieser "
"Gemeinschaft gehören nicht gelöscht."
#: snikket_web/templates/admin_edit_circle.html:24
msgid "Circle members"
msgstr "Mitglieder der Gemeinschaft"
#: snikket_web/templates/admin_edit_circle.html:38
#, python-format
msgid "Remove user %(username)s from circle"
msgstr "Benutzer %(username)s aus der Gemeinschaft entfernen"
#: snikket_web/templates/admin_edit_circle.html:46
msgid "Invite more members"
msgstr "Mehr Mitglieder einladen"
#: snikket_web/templates/admin_edit_invite.html:8 #: snikket_web/templates/admin_edit_invite.html:8
msgid "View invitation" msgid "View invitation"
msgstr "Einladung anzeigen" msgstr "Einladung anzeigen"
#: snikket_web/templates/admin_edit_invite.html:13 #: snikket_web/templates/admin_edit_invite.html:13
#: snikket_web/templates/admin_invites.html:24 #: snikket_web/templates/admin_invites.html:21
msgid "Created"
msgstr "Erzeugt"
#: snikket_web/templates/admin_edit_invite.html:15
#: snikket_web/templates/admin_invites.html:25
msgid "Valid until" msgid "Valid until"
msgstr "Gültig bis" msgstr "Gültig bis"
#: snikket_web/templates/admin_edit_invite.html:17 #: snikket_web/templates/admin_edit_invite.html:15
msgid "Link" msgid "Link"
msgstr "Link" msgstr "Link"
#: snikket_web/templates/admin_edit_invite.html:24 #: snikket_web/templates/admin_edit_invite.html:17
#: snikket_web/templates/user_logout.html:12 msgid "Reusability"
#: snikket_web/templates/user_passwd.html:39 msgstr "Wiederverwendbarkeit"
#: snikket_web/templates/user_profile.html:24
msgid "Back" #: snikket_web/templates/admin_edit_invite.html:18
msgstr "Zurück" msgid ""
"This invitation link can be used arbitrarily often, until it expires, is "
"revoked or a server-wide user limit is reached."
msgstr ""
"Diese Einladung kann beliebig oft verwendet werden, bis sie abläuft, "
"gelöscht wird oder ein instanzweites Limit erreicht ist."
#: snikket_web/templates/admin_edit_invite.html:18
msgid "This invitation link can only be used once and is then depleted."
msgstr "Diese Einladung kann nur einmal verwendet werden und ist dann ungültig."
#: snikket_web/templates/admin_edit_invite.html:22
msgid "Circles"
msgstr "Gemeinschaften"
#: snikket_web/templates/admin_edit_invite.html:23
msgid "Users joining via this invitation will be added to the following circles:"
msgstr ""
"Benutzer die über diese Einladung zur Instanz stoßen werden zu den "
"folgenden Gemeinschaften hinzugefügt:"
#: snikket_web/templates/admin_edit_invite.html:29
#: snikket_web/templates/admin_invites.html:23
msgid "Circle"
msgstr "Gemeinschaft"
#: snikket_web/templates/admin_edit_invite.html:35
msgid "The user will not be added to any circle and will have no contacts."
msgstr ""
"Benutzer werden zu keiner Gemeinschaft hinzugefügt und werden zu Beginn "
"keine Kontakte haben."
#: snikket_web/templates/admin_edit_invite.html:39
msgid "Created"
msgstr "Erzeugt"
#: snikket_web/templates/admin_edit_user.html:3 #: snikket_web/templates/admin_edit_user.html:3
#, python-format #, python-format
@@ -178,7 +373,7 @@ msgid "At your service, %(user_name)s."
msgstr "Zu deinen Diensten, %(user_name)s." msgstr "Zu deinen Diensten, %(user_name)s."
#: snikket_web/templates/admin_home.html:7 #: snikket_web/templates/admin_home.html:7
#: snikket_web/templates/admin_users.html:10 #: snikket_web/templates/admin_users.html:4
msgid "Manage users" msgid "Manage users"
msgstr "Benutzer verwalten" msgstr "Benutzer verwalten"
@@ -186,61 +381,56 @@ msgstr "Benutzer verwalten"
msgid "Modify administrative user information or delete users." msgid "Modify administrative user information or delete users."
msgstr "Benutzerinformationen verändern oder Benutzer löschen." msgstr "Benutzerinformationen verändern oder Benutzer löschen."
#: snikket_web/templates/admin_home.html:11 #: snikket_web/templates/admin_home.html:14
#: snikket_web/templates/admin_invites.html:7 #: snikket_web/templates/admin_invites.html:8
msgid "Manage invitations" msgid "Manage invitations"
msgstr "Einladungen verwalten" msgstr "Einladungen verwalten"
#: snikket_web/templates/admin_home.html:12 #: snikket_web/templates/admin_home.html:15
msgid "Create, revoke or view invitations." msgid "Create, revoke or view invitations."
msgstr "Einladungen erzeugen, löschen oder anzeigen." msgstr "Einladungen erzeugen, löschen oder anzeigen."
#: snikket_web/templates/admin_home.html:15 #: snikket_web/templates/admin_home.html:18
msgid "Back to the main view" msgid "Back to the main view"
msgstr "Zurück zur Hauptseite" msgstr "Zurück zur Hauptseite"
#: snikket_web/templates/admin_home.html:16 #: snikket_web/templates/admin_home.html:19
msgid "Go back to your users web portal page." msgid "Go back to your users web portal page."
msgstr "Zurück zur Startseite deines Benutzers." msgstr "Zurück zur Startseite deines Benutzers."
#: snikket_web/templates/admin_invites.html:10 #: snikket_web/templates/admin_invites.html:10
msgid "Create new invitation"
msgstr "Neue Einladung erzeugen"
#: snikket_web/templates/admin_invites.html:11
msgid ""
"Create a new invitation link to invite more users to your Snikket "
"instance by clicking the button below."
msgstr ""
"Erzeuge eine neue Einladung um mehr Benutzer auf deine Snikket-Instanz "
"einzuladen indem du den folgenden Button klickst."
#: snikket_web/templates/admin_invites.html:16
msgid "Pending invitations" msgid "Pending invitations"
msgstr "Ausstehende Einladungen" msgstr "Ausstehende Einladungen"
#: snikket_web/templates/admin_invites.html:26 #: snikket_web/templates/admin_invites.html:22
#: snikket_web/templates/admin_users.html:18 msgid "Reusable"
msgid "Actions" msgstr "Mehrfach"
msgstr "Aktionen"
#: snikket_web/templates/admin_invites.html:36 #: snikket_web/templates/admin_invites.html:31
msgid "Yes"
msgstr "Ja"
#: snikket_web/templates/admin_invites.html:31
msgid "No"
msgstr "Nein"
#: snikket_web/templates/admin_invites.html:44
msgid "Show invite details" msgid "Show invite details"
msgstr "Einladungsdetails anzeigen" msgstr "Einladungsdetails anzeigen"
#: snikket_web/templates/admin_invites.html:38 #: snikket_web/templates/admin_invites.html:47
msgid "Copy invite link to clipboard" msgid "Copy invite link to clipboard"
msgstr "Einladungslink kopieren" msgstr "Einladungslink kopieren"
#: snikket_web/templates/admin_invites.html:40 #: snikket_web/templates/admin_invites.html:50
msgid "Delete invitation" msgid "Delete invitation"
msgstr "Einladung löschen" msgstr "Einladung löschen"
#: snikket_web/templates/admin_invites.html:48 #: snikket_web/templates/admin_invites.html:58
msgid "Currently, there are no pending invitations." msgid "Currently, there are no pending invitations."
msgstr "Derzeit gibt es keine ausstehenden Einladungen." msgstr "Derzeit gibt es keine ausstehenden Einladungen."
#: snikket_web/templates/admin_users.html:17 #: snikket_web/templates/admin_users.html:11
msgid "Phone number" msgid "Phone number"
msgstr "Telefonnummer" msgstr "Telefonnummer"
@@ -253,14 +443,21 @@ msgstr "Snikket Webportal"
msgid "A <a href=\"%(about_url)s\">Snikket</a> server" msgid "A <a href=\"%(about_url)s\">Snikket</a> server"
msgstr "Ein <a href=\"%(about_url)s\">Snikket</a>-Server" msgstr "Ein <a href=\"%(about_url)s\">Snikket</a>-Server"
#: snikket_web/templates/library.j2:13 #: snikket_web/templates/copy-snippet.html:106
msgid "Copied to clipboard"
msgstr "Kopiert"
#: snikket_web/templates/copy-snippet.html:109
msgid "Copy operation failed"
msgstr "Kopieren fehlgeschlagen"
#: snikket_web/templates/library.j2:18
msgid "Copy link" msgid "Copy link"
msgstr "Link kopieren" msgstr "Link kopieren"
#: snikket_web/templates/library.j2:15 #: snikket_web/templates/library.j2:79
#, python-format msgid "Invalid input"
msgid "Copy &quot;%(content)s&quot; to clipboard" msgstr "Ungültige Eingabe"
msgstr "&quot;%(content)s&quot; kopieren"
#: snikket_web/templates/login.html:5 #: snikket_web/templates/login.html:5
msgid "Snikket Login" msgid "Snikket Login"
@@ -274,10 +471,6 @@ msgstr "Gib deine Snikket-Adresse und -Passwort ein um dein Konto zu verwalten."
msgid "Login failed" msgid "Login failed"
msgstr "Anmeldung fehlgeschlagen" msgstr "Anmeldung fehlgeschlagen"
#: snikket_web/templates/login.html:31
msgid "Log in"
msgstr "Anmelden"
#: snikket_web/templates/user_home.html:3 #: snikket_web/templates/user_home.html:3
msgid "Welcome!" msgid "Welcome!"
msgstr "Willkommen!" msgstr "Willkommen!"
@@ -300,7 +493,7 @@ msgstr ""
"einsehen kann." "einsehen kann."
#: snikket_web/templates/user_home.html:11 #: snikket_web/templates/user_home.html:11
#: snikket_web/templates/user_passwd.html:40 #: snikket_web/templates/user_passwd.html:42
msgid "Change password" msgid "Change password"
msgstr "Passwort ändern" msgstr "Passwort ändern"
@@ -322,11 +515,11 @@ msgstr ""
"Verlasse das Snikket Web-Portal, ohne dass deine anderen Geräte " "Verlasse das Snikket Web-Portal, ohne dass deine anderen Geräte "
"beeinträchtigt werden." "beeinträchtigt werden."
#: snikket_web/templates/user_logout.html:7 #: snikket_web/templates/user_logout.html:8
msgid "Sign out of the Snikket Web Portal" msgid "Sign out of the Snikket Web Portal"
msgstr "Aus dem Webportal abmelden" msgstr "Aus dem Webportal abmelden"
#: snikket_web/templates/user_logout.html:8 #: snikket_web/templates/user_logout.html:9
msgid "" msgid ""
"Click below to log yourself out of the web portal. This does not affect " "Click below to log yourself out of the web portal. This does not affect "
"any other connected devices." "any other connected devices."
@@ -334,15 +527,11 @@ msgstr ""
"Klicke unten um dich aus dem Webportal abzumelden. Dies betrifft keine " "Klicke unten um dich aus dem Webportal abzumelden. Dies betrifft keine "
"anderen Geräte von dir." "anderen Geräte von dir."
#: snikket_web/templates/user_logout.html:14 #: snikket_web/templates/user_passwd.html:8
msgid "Sign out"
msgstr "Abmelden"
#: snikket_web/templates/user_passwd.html:7
msgid "Change your password" msgid "Change your password"
msgstr "Ändere dein Passwort" msgstr "Ändere dein Passwort"
#: snikket_web/templates/user_passwd.html:8 #: snikket_web/templates/user_passwd.html:9
msgid "" msgid ""
"To change your password, you need to provide the current password as well" "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 " " as the new one. To reduce the chance of typos, we ask for your new "
@@ -352,15 +541,15 @@ msgstr ""
"alsauch dein neues Passwort angeben. Damit Tippfehler dich nicht " "alsauch dein neues Passwort angeben. Damit Tippfehler dich nicht "
"aussperren bitten wir dich, dein neues Passwort zweimal einzutippen." "aussperren bitten wir dich, dein neues Passwort zweimal einzutippen."
#: snikket_web/templates/user_passwd.html:12 #: snikket_web/templates/user_passwd.html:13
msgid "Password change failed" msgid "Password change failed"
msgstr "Passwortänderung fehlgeschlagen" msgstr "Passwortänderung fehlgeschlagen"
#: snikket_web/templates/user_passwd.html:35 #: snikket_web/templates/user_passwd.html:36
msgid "Warning" msgid "Warning"
msgstr "Warnung" msgstr "Warnung"
#: snikket_web/templates/user_passwd.html:36 #: snikket_web/templates/user_passwd.html:37
msgid "" msgid ""
"After changing your password, you will have to enter the new password on " "After changing your password, you will have to enter the new password on "
"all of your devices." "all of your devices."
@@ -368,15 +557,15 @@ msgstr ""
"Nachdem du das Passwort geändert hast, musst du das neue Passwort auf " "Nachdem du das Passwort geändert hast, musst du das neue Passwort auf "
"allen Geräten manuell eintragen." "allen Geräten manuell eintragen."
#: snikket_web/templates/user_profile.html:7 #: snikket_web/templates/user_profile.html:8
msgid "Profile" msgid "Profile"
msgstr "Profil" msgstr "Profil"
#: snikket_web/templates/user_profile.html:17 #: snikket_web/templates/user_profile.html:18
msgid "Visibility" msgid "Visibility"
msgstr "Sichtbarkeit" msgstr "Sichtbarkeit"
#: snikket_web/templates/user_profile.html:18 #: snikket_web/templates/user_profile.html:19
msgid "" msgid ""
"This section allows you to control who can see your profile information, " "This section allows you to control who can see your profile information, "
"like avatar and nickname." "like avatar and nickname."
@@ -384,7 +573,3 @@ msgstr ""
"Hier kannst du einstellen, wer deine Profilinformationen, wie Bild oder " "Hier kannst du einstellen, wer deine Profilinformationen, wie Bild oder "
"Anzeigename einsehen kann." "Anzeigename einsehen kann."
#: snikket_web/templates/user_profile.html:24
msgid "Apply"
msgstr "Übernehmen"

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2021-01-17 20:13+0100\n" "POT-Creation-Date: 2021-01-21 16:54+0100\n"
"PO-Revision-Date: 2020-03-07 16:50+0100\n" "PO-Revision-Date: 2020-03-07 16:50+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n" "Language: en\n"
@@ -18,18 +18,74 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.9.0\n" "Generated-By: Babel 2.9.0\n"
#: snikket_web/admin.py:44 #: snikket_web/admin.py:54
msgid "Delete user permanently" msgid "Delete user permanently"
msgstr "" msgstr ""
#: snikket_web/admin.py:71 #: snikket_web/admin.py:83
msgid "Invite to circle"
msgstr ""
#: snikket_web/admin.py:89
msgid "At least one circle must be selected"
msgstr ""
#: snikket_web/admin.py:94
msgid "Valid for"
msgstr ""
#: snikket_web/admin.py:96
msgid "One hour"
msgstr ""
#: snikket_web/admin.py:97
msgid "Twelve hours"
msgstr ""
#: snikket_web/admin.py:98
msgid "One day"
msgstr ""
#: snikket_web/admin.py:99
msgid "One week"
msgstr ""
#: snikket_web/admin.py:100
msgid "Four weeks"
msgstr ""
#: snikket_web/admin.py:106
msgid "Allow multiple uses"
msgstr ""
#: snikket_web/admin.py:110
msgid "New invitation link" msgid "New invitation link"
msgstr "" msgstr ""
#: snikket_web/admin.py:104 #: snikket_web/admin.py:170
msgid "Revoke" msgid "Revoke"
msgstr "" msgstr ""
#: snikket_web/admin.py:225 snikket_web/admin.py:272
msgid "Name"
msgstr ""
#: snikket_web/admin.py:229 snikket_web/templates/admin_circles.html:42
msgid "Create circle"
msgstr ""
#: snikket_web/admin.py:276 snikket_web/user.py:73
msgid "Apply"
msgstr ""
#: snikket_web/admin.py:280
msgid "Delete circle permanently"
msgstr ""
#: snikket_web/infra.py:39
msgid "Main"
msgstr ""
#: snikket_web/main.py:33 #: snikket_web/main.py:33
msgid "Address" msgid "Address"
msgstr "" msgstr ""
@@ -38,7 +94,11 @@ msgstr ""
msgid "Password" msgid "Password"
msgstr "" msgstr ""
#: snikket_web/main.py:60 #: snikket_web/main.py:43
msgid "Sign in"
msgstr ""
#: snikket_web/main.py:64
#, fuzzy #, fuzzy
msgid "Invalid user name or password." msgid "Invalid user name or password."
msgstr "" msgstr ""
@@ -59,38 +119,100 @@ msgstr ""
msgid "The new passwords must match." msgid "The new passwords must match."
msgstr "" msgstr ""
#: snikket_web/user.py:50 #: snikket_web/user.py:47
msgid "Nobody" #, fuzzy
msgstr "" msgid "Sign out"
#: snikket_web/user.py:51
msgid "Friends only"
msgstr "" msgstr ""
#: snikket_web/user.py:52 #: snikket_web/user.py:52
msgid "Nobody"
msgstr ""
#: snikket_web/user.py:53
msgid "Friends only"
msgstr ""
#: snikket_web/user.py:54
msgid "Everyone" msgid "Everyone"
msgstr "" msgstr ""
#: snikket_web/templates/admin_delete_user.html:12 #: snikket_web/templates/admin_delete_user.html:12
#: snikket_web/templates/admin_delete_user.html:16 #: snikket_web/templates/admin_delete_user.html:16
#: snikket_web/templates/admin_users.html:15 snikket_web/user.py:58 #: snikket_web/templates/admin_users.html:9 snikket_web/user.py:60
msgid "Display name" msgid "Display name"
msgstr "" msgstr ""
#: snikket_web/user.py:62 #: snikket_web/user.py:64
msgid "Avatar" msgid "Avatar"
msgstr "" msgstr ""
#: snikket_web/user.py:66 #: snikket_web/user.py:68
msgid "Profile visibility" msgid "Profile visibility"
msgstr "" msgstr ""
#: snikket_web/user.py:91 #: snikket_web/user.py:97
msgid "Incorrect password" msgid "Incorrect password"
msgstr "" msgstr ""
#: snikket_web/templates/admin_circles.html:4
#: snikket_web/templates/admin_home.html:11
msgid "Manage circles"
msgstr ""
#: snikket_web/templates/admin_circles.html:11
msgid "Circle name"
msgstr ""
#: snikket_web/templates/admin_circles.html:12
msgid "Members"
msgstr ""
#: snikket_web/templates/admin_circles.html:13
#: snikket_web/templates/admin_invites.html:24
#: snikket_web/templates/admin_users.html:12
msgid "Actions"
msgstr ""
#: snikket_web/templates/admin_circles.html:23
#, python-format
msgid "Create invitation to circle %(circle_name)s"
msgstr ""
#: snikket_web/templates/admin_circles.html:26
#, python-format
msgid "Show details of circle %(circle_name)s"
msgstr ""
#: snikket_web/templates/admin_circles.html:35
msgid "No circles"
msgstr ""
#: snikket_web/templates/admin_circles.html:36
msgid ""
"Currently, there are no circles on this instance. Use the form below to "
"create one."
msgstr ""
#: snikket_web/templates/admin_circles.html:39
msgid "New circle"
msgstr ""
#: snikket_web/templates/admin_create_invite.html:3
msgid "Create invitation"
msgstr ""
#: snikket_web/templates/admin_create_invite_form.html:5
msgid "Create new invitation"
msgstr ""
#: snikket_web/templates/admin_create_invite_form.html:6
msgid ""
"Create a new invitation link to invite more users to your Snikket "
"instance by clicking the button below."
msgstr ""
#: snikket_web/templates/admin_delete_user.html:4 #: snikket_web/templates/admin_delete_user.html:4
#: snikket_web/templates/admin_users.html:29 #: snikket_web/templates/admin_users.html:24
#, python-format #, python-format
msgid "Delete user %(user_name)s" msgid "Delete user %(user_name)s"
msgstr "" msgstr ""
@@ -105,12 +227,12 @@ msgid "Are you sure you want to delete the following user?"
msgstr "" msgstr ""
#: snikket_web/templates/admin_delete_user.html:10 #: snikket_web/templates/admin_delete_user.html:10
#: snikket_web/templates/admin_users.html:14 #: snikket_web/templates/admin_users.html:8
msgid "Login name" msgid "Login name"
msgstr "" msgstr ""
#: snikket_web/templates/admin_delete_user.html:14 #: snikket_web/templates/admin_delete_user.html:14
#: snikket_web/templates/admin_users.html:16 #: snikket_web/templates/admin_users.html:10
#, fuzzy #, fuzzy
msgid "Email address" msgid "Email address"
msgstr "" msgstr ""
@@ -119,29 +241,91 @@ msgstr ""
msgid "Danger" msgid "Danger"
msgstr "" msgstr ""
#: snikket_web/templates/admin_delete_user.html:23
#: snikket_web/templates/admin_edit_circle.html:14
#: snikket_web/templates/admin_edit_invite.html:45
#: snikket_web/templates/user_logout.html:13
#: snikket_web/templates/user_passwd.html:40
#: snikket_web/templates/user_profile.html:25
msgid "Back"
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:4
#, python-format
msgid "Edit circle %(circle_name)s"
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:6
msgid "Circle information"
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:18
msgid "Delete circle"
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:19
msgid "Deleting a circle does not delete any users in the circle."
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:24
msgid "Circle members"
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:38
#, python-format
msgid "Remove user %(username)s from circle"
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:46
msgid "Invite more members"
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:8 #: snikket_web/templates/admin_edit_invite.html:8
msgid "View invitation" msgid "View invitation"
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_invite.html:13 #: snikket_web/templates/admin_edit_invite.html:13
#: snikket_web/templates/admin_invites.html:24 #: snikket_web/templates/admin_invites.html:21
msgid "Created"
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:15
#: snikket_web/templates/admin_invites.html:25
msgid "Valid until" msgid "Valid until"
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_invite.html:17 #: snikket_web/templates/admin_edit_invite.html:15
msgid "Link" msgid "Link"
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_invite.html:24 #: snikket_web/templates/admin_edit_invite.html:17
#: snikket_web/templates/user_logout.html:12 msgid "Reusability"
#: snikket_web/templates/user_passwd.html:39 msgstr ""
#: snikket_web/templates/user_profile.html:24
msgid "Back" #: snikket_web/templates/admin_edit_invite.html:18
msgid ""
"This invitation link can be used arbitrarily often, until it expires, is "
"revoked or a server-wide user limit is reached."
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:18
msgid "This invitation link can only be used once and is then depleted."
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:22
msgid "Circles"
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:23
msgid "Users joining via this invitation will be added to the following circles:"
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:29
#: snikket_web/templates/admin_invites.html:23
msgid "Circle"
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:35
msgid "The user will not be added to any circle and will have no contacts."
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:39
msgid "Created"
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_user.html:3 #: snikket_web/templates/admin_edit_user.html:3
@@ -176,7 +360,7 @@ msgid "At your service, %(user_name)s."
msgstr "" msgstr ""
#: snikket_web/templates/admin_home.html:7 #: snikket_web/templates/admin_home.html:7
#: snikket_web/templates/admin_users.html:10 #: snikket_web/templates/admin_users.html:4
msgid "Manage users" msgid "Manage users"
msgstr "" msgstr ""
@@ -184,59 +368,56 @@ msgstr ""
msgid "Modify administrative user information or delete users." msgid "Modify administrative user information or delete users."
msgstr "" msgstr ""
#: snikket_web/templates/admin_home.html:11 #: snikket_web/templates/admin_home.html:14
#: snikket_web/templates/admin_invites.html:7 #: snikket_web/templates/admin_invites.html:8
msgid "Manage invitations" msgid "Manage invitations"
msgstr "" msgstr ""
#: snikket_web/templates/admin_home.html:12 #: snikket_web/templates/admin_home.html:15
msgid "Create, revoke or view invitations." msgid "Create, revoke or view invitations."
msgstr "" msgstr ""
#: snikket_web/templates/admin_home.html:15 #: snikket_web/templates/admin_home.html:18
msgid "Back to the main view" msgid "Back to the main view"
msgstr "" msgstr ""
#: snikket_web/templates/admin_home.html:16 #: snikket_web/templates/admin_home.html:19
msgid "Go back to your users web portal page." msgid "Go back to your users web portal page."
msgstr "" msgstr ""
#: snikket_web/templates/admin_invites.html:10 #: snikket_web/templates/admin_invites.html:10
msgid "Create new invitation"
msgstr ""
#: snikket_web/templates/admin_invites.html:11
msgid ""
"Create a new invitation link to invite more users to your Snikket "
"instance by clicking the button below."
msgstr ""
#: snikket_web/templates/admin_invites.html:16
msgid "Pending invitations" msgid "Pending invitations"
msgstr "" msgstr ""
#: snikket_web/templates/admin_invites.html:26 #: snikket_web/templates/admin_invites.html:22
#: snikket_web/templates/admin_users.html:18 msgid "Reusable"
msgid "Actions"
msgstr "" msgstr ""
#: snikket_web/templates/admin_invites.html:36 #: snikket_web/templates/admin_invites.html:31
msgid "Yes"
msgstr ""
#: snikket_web/templates/admin_invites.html:31
msgid "No"
msgstr ""
#: snikket_web/templates/admin_invites.html:44
msgid "Show invite details" msgid "Show invite details"
msgstr "" msgstr ""
#: snikket_web/templates/admin_invites.html:38 #: snikket_web/templates/admin_invites.html:47
msgid "Copy invite link to clipboard" msgid "Copy invite link to clipboard"
msgstr "" msgstr ""
#: snikket_web/templates/admin_invites.html:40 #: snikket_web/templates/admin_invites.html:50
msgid "Delete invitation" msgid "Delete invitation"
msgstr "" msgstr ""
#: snikket_web/templates/admin_invites.html:48 #: snikket_web/templates/admin_invites.html:58
msgid "Currently, there are no pending invitations." msgid "Currently, there are no pending invitations."
msgstr "" msgstr ""
#: snikket_web/templates/admin_users.html:17 #: snikket_web/templates/admin_users.html:11
msgid "Phone number" msgid "Phone number"
msgstr "" msgstr ""
@@ -249,13 +430,20 @@ msgstr ""
msgid "A <a href=\"%(about_url)s\">Snikket</a> server" msgid "A <a href=\"%(about_url)s\">Snikket</a> server"
msgstr "" msgstr ""
#: snikket_web/templates/library.j2:13 #: snikket_web/templates/copy-snippet.html:106
msgid "Copied to clipboard"
msgstr ""
#: snikket_web/templates/copy-snippet.html:109
msgid "Copy operation failed"
msgstr ""
#: snikket_web/templates/library.j2:18
msgid "Copy link" msgid "Copy link"
msgstr "" msgstr ""
#: snikket_web/templates/library.j2:15 #: snikket_web/templates/library.j2:79
#, python-format msgid "Invalid input"
msgid "Copy &quot;%(content)s&quot; to clipboard"
msgstr "" msgstr ""
#: snikket_web/templates/login.html:5 #: snikket_web/templates/login.html:5
@@ -270,10 +458,6 @@ msgstr ""
msgid "Login failed" msgid "Login failed"
msgstr "" msgstr ""
#: snikket_web/templates/login.html:31
msgid "Log in"
msgstr ""
#: snikket_web/templates/user_home.html:3 #: snikket_web/templates/user_home.html:3
msgid "Welcome!" msgid "Welcome!"
msgstr "" msgstr ""
@@ -294,7 +478,7 @@ msgid ""
msgstr "" msgstr ""
#: snikket_web/templates/user_home.html:11 #: snikket_web/templates/user_home.html:11
#: snikket_web/templates/user_passwd.html:40 #: snikket_web/templates/user_passwd.html:42
msgid "Change password" msgid "Change password"
msgstr "" msgstr ""
@@ -314,60 +498,58 @@ msgstr ""
msgid "Exit the Snikket Web Portal, without logging out your other devices." msgid "Exit the Snikket Web Portal, without logging out your other devices."
msgstr "" msgstr ""
#: snikket_web/templates/user_logout.html:7 #: snikket_web/templates/user_logout.html:8
msgid "Sign out of the Snikket Web Portal" msgid "Sign out of the Snikket Web Portal"
msgstr "" msgstr ""
#: snikket_web/templates/user_logout.html:8 #: snikket_web/templates/user_logout.html:9
msgid "" msgid ""
"Click below to log yourself out of the web portal. This does not affect " "Click below to log yourself out of the web portal. This does not affect "
"any other connected devices." "any other connected devices."
msgstr "" msgstr ""
#: snikket_web/templates/user_logout.html:14 #: snikket_web/templates/user_passwd.html:8
#, fuzzy
msgid "Sign out"
msgstr ""
#: snikket_web/templates/user_passwd.html:7
msgid "Change your password" msgid "Change your password"
msgstr "" msgstr ""
#: snikket_web/templates/user_passwd.html:8 #: snikket_web/templates/user_passwd.html:9
msgid "" msgid ""
"To change your password, you need to provide the current password as well" "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 " " as the new one. To reduce the chance of typos, we ask for your new "
"password twice." "password twice."
msgstr "" msgstr ""
#: snikket_web/templates/user_passwd.html:12 #: snikket_web/templates/user_passwd.html:13
msgid "Password change failed" msgid "Password change failed"
msgstr "" msgstr ""
#: snikket_web/templates/user_passwd.html:35 #: snikket_web/templates/user_passwd.html:36
msgid "Warning" msgid "Warning"
msgstr "" msgstr ""
#: snikket_web/templates/user_passwd.html:36 #: snikket_web/templates/user_passwd.html:37
msgid "" msgid ""
"After changing your password, you will have to enter the new password on " "After changing your password, you will have to enter the new password on "
"all of your devices." "all of your devices."
msgstr "" msgstr ""
#: snikket_web/templates/user_profile.html:7 #: snikket_web/templates/user_profile.html:8
msgid "Profile" msgid "Profile"
msgstr "" msgstr ""
#: snikket_web/templates/user_profile.html:17 #: snikket_web/templates/user_profile.html:18
msgid "Visibility" msgid "Visibility"
msgstr "" msgstr ""
#: snikket_web/templates/user_profile.html:18 #: snikket_web/templates/user_profile.html:19
msgid "" msgid ""
"This section allows you to control who can see your profile information, " "This section allows you to control who can see your profile information, "
"like avatar and nickname." "like avatar and nickname."
msgstr "" msgstr ""
#: snikket_web/templates/user_profile.html:24 #~ msgid "Copy &quot;%(content)s&quot; to clipboard"
msgid "Apply" #~ msgstr ""
msgstr ""
#~ msgid "Log in"
#~ msgstr ""

View File

@@ -43,7 +43,9 @@ class ChangePasswordForm(flask_wtf.FlaskForm): # type:ignore
class LogoutForm(flask_wtf.FlaskForm): # type:ignore class LogoutForm(flask_wtf.FlaskForm): # type:ignore
pass action_signout = wtforms.SubmitField(
_l("Sign out"),
)
_ACCESS_MODEL_CHOICES = [ _ACCESS_MODEL_CHOICES = [
@@ -106,6 +108,9 @@ async def profile() -> typing.Union[str, quart.Response]:
form = ProfileForm() form = ProfileForm()
if request.method != "POST": if request.method != "POST":
user_info = await client.get_user_info() user_info = await client.get_user_info()
# TODO: find a better way to determine the access model, e.g. by
# taking the first access model which is defined in [nickname, avatar,
# vcard] or by taking the most open one.-
try: try:
profile_access_model = await client.get_nickname_access_model() profile_access_model = await client.get_nickname_access_model()
except quart.exceptions.NotFound: except quart.exceptions.NotFound: