diff --git a/snikket_web/admin.py b/snikket_web/admin.py index c8ad4ad..dd2dc96 100644 --- a/snikket_web/admin.py +++ b/snikket_web/admin.py @@ -1,18 +1,28 @@ +import asyncio import typing from datetime import datetime +import aiohttp + import quart.flask_patch import wtforms 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 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") @@ -67,10 +77,55 @@ async def delete_user(localpart: str) -> typing.Union[str, quart.Response]: class InvitesListForm(flask_wtf.FlaskForm): # type:ignore 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( _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"]) @client.require_admin_session() @@ -78,23 +133,34 @@ async def invitations() -> typing.Union[str, quart.Response]: user_info = await client.get_user_info() invites = sorted( 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() if form.validate_on_submit(): if 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 await render_template( "admin_invites.html", user_info=user_info, invites=invites, + invite_form=invite_form, now=datetime.utcnow(), + circle_map=circle_map, 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/", methods=["GET", "POST"]) @client.require_admin_session() async def edit_invite(id_: str) -> typing.Union[str, quart.Response]: user_info = await client.get_user_info() 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() if form.validate_on_submit(): @@ -124,4 +216,120 @@ async def edit_invite(id_: str) -> typing.Union[str, quart.Response]: invite=invite_info, now=datetime.utcnow(), 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/", 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, ) diff --git a/snikket_web/infra.py b/snikket_web/infra.py index 8a5028e..7021c16 100644 --- a/snikket_web/infra.py +++ b/snikket_web/infra.py @@ -8,6 +8,7 @@ from quart import ( ) import flask_babel +from flask_babel import _ from . import prosodyclient @@ -20,9 +21,11 @@ babel = flask_babel.Babel() @babel.localeselector # type:ignore def selected_locale() -> str: - return request.accept_languages.best_match( + selected = request.accept_languages.best_match( 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: @@ -31,6 +34,12 @@ def flatten(a: typing.Iterable, levels: int = 1) -> typing.Iterable: 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: app.template_filter("repr")(repr) 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_timedelta")(flask_babel.format_timedelta) app.template_filter("flatten")(flatten) + app.template_filter("circle_name")(circle_name) diff --git a/snikket_web/main.py b/snikket_web/main.py index a19944c..a42297e 100644 --- a/snikket_web/main.py +++ b/snikket_web/main.py @@ -39,6 +39,10 @@ class LoginForm(flask_wtf.FlaskForm): # type:ignore validators=[wtforms.validators.InputRequired()], ) + action_signin = wtforms.SubmitField( + _l("Sign in"), + ) + @bp.route("/login", methods=["GET", "POST"]) async def login() -> typing.Union[str, quart.Response]: diff --git a/snikket_web/prosodyclient.py b/snikket_web/prosodyclient.py index c110a3b..67d30d0 100644 --- a/snikket_web/prosodyclient.py +++ b/snikket_web/prosodyclient.py @@ -72,6 +72,8 @@ class AdminInviteInfo: landing_page: typing.Optional[str] created_at: datetime expires: datetime + reusable: bool + group_ids: typing.Collection[str] @classmethod def from_api_response( @@ -87,6 +89,26 @@ class AdminInviteInfo: token=data["id"], xmpp_uri=data.get("xmpp_uri"), 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: return self.SESSION_TOKEN in http_session + def authenticated_session(self) -> HTTPAuthSessionManager: + return self._auth_session + def require_session( self, redirect_to: typing.Optional[str] = None, @@ -394,29 +419,33 @@ class ProsodyClient: ) 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) - async with self._auth_session as session: - nickname = await self.get_user_nickname(session=session) - try: - avatar_info = await self.get_avatar( - self.session_address, - metadata_only=True, - session=session, - ) - avatar_hash = avatar_info["sha1"] - except quart.exceptions.HTTPException: - avatar_hash = None + nickname = await self.get_user_nickname(session=session) + try: + avatar_info = await self.get_avatar( + self.session_address, + metadata_only=True, + session=session, + ) + avatar_hash = avatar_info["sha1"] + except quart.exceptions.HTTPException: + avatar_hash = None - return { - "address": self.session_address, - "username": localpart, - "nickname": nickname, - "display_name": nickname or localpart, - "avatar_hash": avatar_hash, - "is_admin": self.is_admin_session, - } + return { + "address": self.session_address, + "username": localpart, + "nickname": nickname, + "display_name": nickname or localpart, + "avatar_hash": avatar_hash, + "is_admin": self.is_admin_session, + } @autosession async def test_session(self, session: aiohttp.ClientSession) -> bool: @@ -698,7 +727,7 @@ class ProsodyClient: if resp.status == 400: abort(500, "request rejected by backend") if not 200 <= resp.status < 300: - abort(resp.status) + resp.raise_for_status() @autosession async def list_users( @@ -777,13 +806,89 @@ class ProsodyClient: @autosession async def create_invite( self, + group_ids: typing.Collection[str], + reusable: bool, + ttl: int, *, session: aiohttp.ClientSession, ) -> 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) 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: # this currently only kills the cookie stuff, we may want to invalidate # the token on the server side, toos diff --git a/snikket_web/scss/_theme.scss b/snikket_web/scss/_theme.scss index b86c51f..4a7b33e 100644 --- a/snikket_web/scss/_theme.scss +++ b/snikket_web/scss/_theme.scss @@ -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%]; $small-screen-threshold: 40rem; +$medium-screen-threshold: 60rem; diff --git a/snikket_web/scss/app.scss b/snikket_web/scss/app.scss index caa2f43..98fd303 100644 --- a/snikket_web/scss/app.scss +++ b/snikket_web/scss/app.scss @@ -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 { display: block; overflow: hidden; @@ -820,15 +836,47 @@ table { 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 */ -@media screen and (max-width: $small-screen-threshold) { - main > .form.layout-expanded { +@media screen and (max-width: $medium-screen-threshold) { + .form.layout-expanded { margin-left: 0; margin-right: 0; } + div.elevated { + margin-left: 0; + margin-right: 0; + } +} + +@media screen and (max-width: $small-screen-threshold) { .form.layout-expanded .box { margin-left: 0; margin-right: 0; diff --git a/snikket_web/templates/admin_circles.html b/snikket_web/templates/admin_circles.html new file mode 100644 index 0000000..ce150a6 --- /dev/null +++ b/snikket_web/templates/admin_circles.html @@ -0,0 +1,51 @@ +{% extends "admin_app.html" %} +{% from "library.j2" import action_button, custom_form_button, form_button, circle_name %} +{% block content %} +

{% trans %}Manage circles{% endtrans %}

+{%- if circles -%} +
+{{- invite_form.csrf_token -}} +
+ + + + + + + + +{% for circle in circles %} + + + + + +{% endfor %} + +
{% trans %}Circle name{% endtrans %}{% trans %}Members{% endtrans %}{% trans %}Actions{% endtrans %}
{{ circle | circle_name }}{{ circle.members | length }} + {%- 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 -%} +
+{%- else -%} +
+
{% trans %}No circles{% endtrans %}
+

{% trans %}Currently, there are no circles on this instance. Use the form below to create one.{% endtrans %}

+
+{%- endif -%} +

{% trans %}New circle{% endtrans %}

+
+{{- create_form.csrf_token -}} +

{% trans %}Create circle{% endtrans %}

+
+ {{- create_form.name.label -}} + {{- create_form.name -}} +
+
+ {%- call form_button("create_group", create_form.action_create, class="primary") -%}{%- endcall -%} +
+
+{% endblock %} diff --git a/snikket_web/templates/admin_create_invite.html b/snikket_web/templates/admin_create_invite.html new file mode 100644 index 0000000..99fe196 --- /dev/null +++ b/snikket_web/templates/admin_create_invite.html @@ -0,0 +1,5 @@ +{% extends "admin_app.html" %} +{% block content %} +

{% trans %}Create invitation{% endtrans %}

+{%- include "admin_create_invite_form.html" -%} +{% endblock %} diff --git a/snikket_web/templates/admin_create_invite_form.html b/snikket_web/templates/admin_create_invite_form.html new file mode 100644 index 0000000..b229884 --- /dev/null +++ b/snikket_web/templates/admin_create_invite_form.html @@ -0,0 +1,31 @@ +{% from "library.j2" import form_button, render_errors %} +
+{{- invite_form.csrf_token -}} +
+

{% trans %}Create new invitation{% endtrans %}

+

{% trans %}Create a new invitation link to invite more users to your Snikket instance by clicking the button below.{% endtrans %}

+
+ {{ invite_form.reusable }} + {{ invite_form.reusable.label }} +
+
+ {{ invite_form.lifetime.label }} +
{{ invite_form.lifetime }}
+
+
+{# + 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 -}} +
{{ invite_form.circles }}
+ {%- call render_errors(invite_form.circles) -%}{%- endcall -%} +
+
+ {%- call form_button("create_link", invite_form.action_create_invite, class="primary") %}{% endcall -%} +
+
diff --git a/snikket_web/templates/admin_edit_circle.html b/snikket_web/templates/admin_edit_circle.html new file mode 100644 index 0000000..cf05258 --- /dev/null +++ b/snikket_web/templates/admin_edit_circle.html @@ -0,0 +1,48 @@ +{% extends "admin_app.html" %} +{% from "library.j2" import form_button, standard_button, value_or_hint, custom_form_button %} +{% block content %} +

{% trans circle_name=(target_circle | circle_name) %}Edit circle {{ circle_name }}{% endtrans %}

+
+

{% trans %}Circle information{% endtrans %}

+ {{ form.csrf_token }} +
+ {{ form.name.label }} + {{ form.name }} +
+
+ {%- call standard_button("back", url_for(".circles"), class="secondary") -%} + {% trans %}Back{% endtrans %} + {%- endcall -%} + {%- call form_button("done", form.action_save, class="primary") %}{% endcall -%} +
+

{% trans %}Delete circle{% endtrans %}

+

{% trans %}Deleting a circle does not delete any users in the circle.{% endtrans %}

+
+ {%- call form_button("done", form.action_delete, class="secondary danger") %}{% endcall -%} +
+
+

{% trans %}Circle members{% endtrans %}

+
+ + + + + + +{%- for member in circle_members -%} + + + + + +{%- endfor -%} + +
Login nameDisplay nameActions
{{ member.localpart }}{% call value_or_hint(member.display_name) %}{% endcall %} + {%- 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 -%} +
+ +

{% trans %}Invite more members{% endtrans %}

+{%- include "admin_create_invite_form.html" -%} +{% endblock %} diff --git a/snikket_web/templates/admin_edit_invite.html b/snikket_web/templates/admin_edit_invite.html index b1ceaaf..23efd4f 100644 --- a/snikket_web/templates/admin_edit_invite.html +++ b/snikket_web/templates/admin_edit_invite.html @@ -10,12 +10,34 @@ {{ form.csrf_token }}
-
{% trans %}Created{% endtrans %}
-
{{ invite.created_at | format_date }}
{% trans %}Valid until{% endtrans %}
{{ invite.expires | format_date }}
{% trans %}Link{% endtrans %}
{% call showuri(invite.landing_page) %}{% endcall %}
+
{% trans %}Reusability{% endtrans %}
+
{% 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 %}
+ {%- set ngroups = invite.group_ids | length -%} + {%- if ngroups > 1 -%} + {#- not supported via the web UI, but we should still display it properly -#} +
{% trans %}Circles{% endtrans %}
+

{% trans %}Users joining via this invitation will be added to the following circles:{% endtrans %}

    + {%- for group_id in invite.group_ids -%} +
  • {{ circle_map[group_id] | circle_name }}
  • + {%- endfor -%} +
+ {%- else -%} +
{% trans %}Circle{% endtrans %}
+
+ {%- if ngroups == 1 -%} + {%- set group_id = invite.group_ids[0] -%} + {{ circle_map[invite.group_ids[0]] | circle_name }} + {%- else -%} + {% trans %}The user will not be added to any circle and will have no contacts.{% endtrans %} + {%- endif -%} +
+ {%- endif -%} +
{% trans %}Created{% endtrans %}
+
{{ invite.created_at | format_date }}
{%- call form_button("remove_link", form.action_revoke, class="secondary danger") %}{% endcall -%} diff --git a/snikket_web/templates/admin_home.html b/snikket_web/templates/admin_home.html index a4d5d1a..8b7e866 100644 --- a/snikket_web/templates/admin_home.html +++ b/snikket_web/templates/admin_home.html @@ -7,6 +7,9 @@

{% trans %}Manage users{% endtrans %}

{% trans %}Modify administrative user information or delete users.{% endtrans %}

+ +

{% trans %}Manage circles{% endtrans %}

+

{% trans %}Manage invitations{% endtrans %}

{% trans %}Create, revoke or view invitations.{% endtrans %}

diff --git a/snikket_web/templates/admin_invites.html b/snikket_web/templates/admin_invites.html index 38007dc..8edbcef 100644 --- a/snikket_web/templates/admin_invites.html +++ b/snikket_web/templates/admin_invites.html @@ -6,33 +6,40 @@ {% endblock %} {% block content %}

{% trans %}Manage invitations{% endtrans %}

-
{{ form.csrf_token }} -
-

{% trans %}Create new invitation{% endtrans %}

-

{% trans %}Create a new invitation link to invite more users to your Snikket instance by clicking the button below.{% endtrans %}

-
- {%- call form_button("create_link", form.action_create_invite, class="primary") %}{% endcall -%} -
-
+{%- include "admin_create_invite_form.html" -%}

{% trans %}Pending invitations{% endtrans %}

{% if invites %} - - + +{{- form.csrf_token -}} +
+ + - + + {% for invite in invites %} - - + + {% endfor %} -
{% trans %}Created{% endtrans %} {% trans %}Valid until{% endtrans %}{% trans %}Reusable{% endtrans %}{% trans %}Circle{% endtrans %} {% trans %}Actions{% endtrans %}
{{ invite.created_at | format_date }} {{ (invite.expires - now) | format_timedelta(add_direction=True) }} + {% if invite.reusable %}{% trans %}Yes{% endtrans %}{% else %}{% trans + %}No{% endtrans %}{% endif %} + {#- -#} +
    + {%- for group_id in invite.group_ids -%} +
  • {{ circle_map[group_id] | circle_name }}
  • + {%- endfor -%} +
+ {#- -#} +
{%- call action_button("more", url_for(".edit_invite", id_=invite.id_), class="secondary") -%} {% trans %}Show invite details{% endtrans %} {%- endcall -%} @@ -46,9 +53,8 @@
+
{% else %}

{% trans %}Currently, there are no pending invitations.{% endtrans %}

{% endif %} - {% endblock %} diff --git a/snikket_web/templates/admin_users.html b/snikket_web/templates/admin_users.html index dcc4d63..31aebe4 100644 --- a/snikket_web/templates/admin_users.html +++ b/snikket_web/templates/admin_users.html @@ -1,15 +1,8 @@ {% extends "admin_app.html" %} -{% from "library.j2" import icon %} -{% macro value_or_hint(v, caller=None) %} -{%- if v is not none -%} -{{- v -}} -{%- else -%} -— -{%- endif -%} -{% endmacro %} +{% from "library.j2" import action_button, value_or_hint %} {% block content %}

{% trans %}Manage users{% endtrans %}

- +
@@ -27,11 +20,12 @@ {% endfor %} -
{% trans %}Login name{% endtrans %}{% call value_or_hint(user.email) %}{% endcall %} {% call value_or_hint(user.phone) %}{% endcall %} - {#- -#}{% call icon("remove") %}{% trans user_name=user.localpart %}Delete user {{ user_name }}{% endtrans %}{% endcall %} - {#- -#} + {%- call action_button("remove", url_for(".delete_user", localpart=user.localpart), class="secondary") -%} + {% trans user_name=user.localpart %}Delete user {{ user_name }}{% endtrans %} + {%- endcall -%}
+
{% endblock %} diff --git a/snikket_web/templates/library.j2 b/snikket_web/templates/library.j2 index 77d0a9e..b2f853e 100644 --- a/snikket_web/templates/library.j2 +++ b/snikket_web/templates/library.j2 @@ -72,3 +72,28 @@ {% endif -%} {%- endmacro %} + +{% macro render_errors(field, caller=None) -%} +{%- if field.errors -%} +
{#- -#} +
{% trans %}Invalid input{% endtrans %}
+{%- if field.errors | length == 1 -%} +

{{ field.errors[0] }}.

+{%- else -%} +
    +{%- for error in field.errors -%} +
  • {{ error }}
  • +{%- endfor -%} +
+{%- endif -%} +
+{%- endif -%} +{%- endmacro %} + +{% macro value_or_hint(v, caller=None) %} +{%- if v is not none -%} +{{- v -}} +{%- else -%} +— +{%- endif -%} +{% endmacro %} diff --git a/snikket_web/templates/login.html b/snikket_web/templates/login.html index e96ce08..24c3691 100644 --- a/snikket_web/templates/login.html +++ b/snikket_web/templates/login.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% from "library.j2" import box, icon %} +{% from "library.j2" import box, form_button %} {% set body_id = "login" %} {% block head_lead %} {{ _("Snikket Login") }} @@ -28,7 +28,7 @@ {{ form.password(placeholder=form.password.label.text) }}
- + {%- call form_button("login", form.action_signin, class="primary") -%}{% endcall -%}
diff --git a/snikket_web/templates/user_logout.html b/snikket_web/templates/user_logout.html index b33cb7e..ba4ee5a 100644 --- a/snikket_web/templates/user_logout.html +++ b/snikket_web/templates/user_logout.html @@ -1,5 +1,5 @@ {% extends "app.html" %} -{% from "library.j2" import icon %} +{% from "library.j2" import standard_button, form_button %} {% block head_lead %} Snikket Web Portal {% endblock %} @@ -9,11 +9,10 @@

{% trans %}Click below to log yourself out of the web portal. This does not affect any other connected devices.{% endtrans %}

{{ form.csrf_token }}
- {#- -#} - {% call icon("back") %}{% endcall %}{% trans %}Back{% endtrans %} - {#- -#} - - {#- -#} + {%- call standard_button("back", url_for("user.index"), class="secondary") -%} + {% trans %}Back{% endtrans %} + {%- endcall -%} + {%- call form_button("logout", form.action_signout, class="primary") %}{% endcall -%}
{% endblock %} diff --git a/snikket_web/translations/de/LC_MESSAGES/messages.po b/snikket_web/translations/de/LC_MESSAGES/messages.po index 48d5aba..1a720ae 100644 --- a/snikket_web/translations/de/LC_MESSAGES/messages.po +++ b/snikket_web/translations/de/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: SnikketWeb 0.1.0\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" "Last-Translator: Jonas Schäfer \n" "Language: de\n" @@ -18,18 +18,74 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.9.0\n" -#: snikket_web/admin.py:44 +#: snikket_web/admin.py:54 msgid "Delete user permanently" 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" msgstr "Neuer Einladungslink" -#: snikket_web/admin.py:104 +#: snikket_web/admin.py:170 msgid "Revoke" 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 msgid "Address" msgstr "Adresse" @@ -38,7 +94,11 @@ msgstr "Adresse" msgid "Password" 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." msgstr "Benutzername oder Passwort falsch." @@ -58,38 +118,103 @@ msgstr "Neues Passwort (Bestätigung)" msgid "The new passwords must match." 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" msgstr "Niemand" -#: snikket_web/user.py:51 +#: snikket_web/user.py:53 msgid "Friends only" msgstr "Nur Freunde" -#: snikket_web/user.py:52 +#: snikket_web/user.py:54 msgid "Everyone" msgstr "Jeder" #: snikket_web/templates/admin_delete_user.html:12 #: 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" msgstr "Anzeigename" -#: snikket_web/user.py:62 +#: snikket_web/user.py:64 msgid "Avatar" msgstr "Bild" -#: snikket_web/user.py:66 +#: snikket_web/user.py:68 msgid "Profile visibility" msgstr "Profilsichtbarkeit" -#: snikket_web/user.py:91 +#: snikket_web/user.py:97 msgid "Incorrect password" 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_users.html:29 +#: snikket_web/templates/admin_users.html:24 #, python-format msgid "Delete user %(user_name)s" 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?" #: 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" msgstr "Anmeldename" #: 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" msgstr "E-Mail-Adresse" @@ -117,30 +242,100 @@ msgstr "E-Mail-Adresse" msgid "Danger" 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 msgid "View invitation" msgstr "Einladung anzeigen" #: snikket_web/templates/admin_edit_invite.html:13 -#: snikket_web/templates/admin_invites.html:24 -msgid "Created" -msgstr "Erzeugt" - -#: snikket_web/templates/admin_edit_invite.html:15 -#: snikket_web/templates/admin_invites.html:25 +#: snikket_web/templates/admin_invites.html:21 msgid "Valid until" msgstr "Gültig bis" -#: snikket_web/templates/admin_edit_invite.html:17 +#: snikket_web/templates/admin_edit_invite.html:15 msgid "Link" msgstr "Link" -#: snikket_web/templates/admin_edit_invite.html:24 -#: snikket_web/templates/user_logout.html:12 -#: snikket_web/templates/user_passwd.html:39 -#: snikket_web/templates/user_profile.html:24 -msgid "Back" -msgstr "Zurück" +#: snikket_web/templates/admin_edit_invite.html:17 +msgid "Reusability" +msgstr "Wiederverwendbarkeit" + +#: 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 "" +"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 #, python-format @@ -178,7 +373,7 @@ msgid "At your service, %(user_name)s." msgstr "Zu deinen Diensten, %(user_name)s." #: snikket_web/templates/admin_home.html:7 -#: snikket_web/templates/admin_users.html:10 +#: snikket_web/templates/admin_users.html:4 msgid "Manage users" msgstr "Benutzer verwalten" @@ -186,61 +381,56 @@ msgstr "Benutzer verwalten" msgid "Modify administrative user information or delete users." msgstr "Benutzerinformationen verändern oder Benutzer löschen." -#: snikket_web/templates/admin_home.html:11 -#: snikket_web/templates/admin_invites.html:7 +#: snikket_web/templates/admin_home.html:14 +#: snikket_web/templates/admin_invites.html:8 msgid "Manage invitations" msgstr "Einladungen verwalten" -#: snikket_web/templates/admin_home.html:12 +#: snikket_web/templates/admin_home.html:15 msgid "Create, revoke or view invitations." 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" msgstr "Zurück zur Hauptseite" -#: snikket_web/templates/admin_home.html:16 +#: snikket_web/templates/admin_home.html:19 msgid "Go back to your user’s web portal page." msgstr "Zurück zur Startseite deines Benutzers." #: 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" msgstr "Ausstehende Einladungen" -#: snikket_web/templates/admin_invites.html:26 -#: snikket_web/templates/admin_users.html:18 -msgid "Actions" -msgstr "Aktionen" +#: snikket_web/templates/admin_invites.html:22 +msgid "Reusable" +msgstr "Mehrfach" -#: 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" msgstr "Einladungsdetails anzeigen" -#: snikket_web/templates/admin_invites.html:38 +#: snikket_web/templates/admin_invites.html:47 msgid "Copy invite link to clipboard" msgstr "Einladungslink kopieren" -#: snikket_web/templates/admin_invites.html:40 +#: snikket_web/templates/admin_invites.html:50 msgid "Delete invitation" 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." msgstr "Derzeit gibt es keine ausstehenden Einladungen." -#: snikket_web/templates/admin_users.html:17 +#: snikket_web/templates/admin_users.html:11 msgid "Phone number" msgstr "Telefonnummer" @@ -253,14 +443,21 @@ msgstr "Snikket Webportal" msgid "A Snikket server" msgstr "Ein Snikket-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" msgstr "Link kopieren" -#: snikket_web/templates/library.j2:15 -#, python-format -msgid "Copy "%(content)s" to clipboard" -msgstr ""%(content)s" kopieren" +#: snikket_web/templates/library.j2:79 +msgid "Invalid input" +msgstr "Ungültige Eingabe" #: snikket_web/templates/login.html:5 msgid "Snikket Login" @@ -274,10 +471,6 @@ msgstr "Gib deine Snikket-Adresse und -Passwort ein um dein Konto zu verwalten." msgid "Login failed" msgstr "Anmeldung fehlgeschlagen" -#: snikket_web/templates/login.html:31 -msgid "Log in" -msgstr "Anmelden" - #: snikket_web/templates/user_home.html:3 msgid "Welcome!" msgstr "Willkommen!" @@ -300,7 +493,7 @@ msgstr "" "einsehen kann." #: snikket_web/templates/user_home.html:11 -#: snikket_web/templates/user_passwd.html:40 +#: snikket_web/templates/user_passwd.html:42 msgid "Change password" msgstr "Passwort ändern" @@ -322,11 +515,11 @@ msgstr "" "Verlasse das Snikket Web-Portal, ohne dass deine anderen Geräte " "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" msgstr "Aus dem Webportal abmelden" -#: snikket_web/templates/user_logout.html:8 +#: snikket_web/templates/user_logout.html:9 msgid "" "Click below to log yourself out of the web portal. This does not affect " "any other connected devices." @@ -334,15 +527,11 @@ msgstr "" "Klicke unten um dich aus dem Webportal abzumelden. Dies betrifft keine " "anderen Geräte von dir." -#: snikket_web/templates/user_logout.html:14 -msgid "Sign out" -msgstr "Abmelden" - -#: snikket_web/templates/user_passwd.html:7 +#: snikket_web/templates/user_passwd.html:8 msgid "Change your password" msgstr "Ändere dein Passwort" -#: snikket_web/templates/user_passwd.html:8 +#: snikket_web/templates/user_passwd.html:9 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 " @@ -352,15 +541,15 @@ msgstr "" "alsauch dein neues Passwort angeben. Damit Tippfehler dich nicht " "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" msgstr "Passwortänderung fehlgeschlagen" -#: snikket_web/templates/user_passwd.html:35 +#: snikket_web/templates/user_passwd.html:36 msgid "Warning" msgstr "Warnung" -#: snikket_web/templates/user_passwd.html:36 +#: snikket_web/templates/user_passwd.html:37 msgid "" "After changing your password, you will have to enter the new password on " "all of your devices." @@ -368,15 +557,15 @@ msgstr "" "Nachdem du das Passwort geändert hast, musst du das neue Passwort auf " "allen Geräten manuell eintragen." -#: snikket_web/templates/user_profile.html:7 +#: snikket_web/templates/user_profile.html:8 msgid "Profile" msgstr "Profil" -#: snikket_web/templates/user_profile.html:17 +#: snikket_web/templates/user_profile.html:18 msgid "Visibility" msgstr "Sichtbarkeit" -#: snikket_web/templates/user_profile.html:18 +#: snikket_web/templates/user_profile.html:19 msgid "" "This section allows you to control who can see your profile information, " "like avatar and nickname." @@ -384,7 +573,3 @@ msgstr "" "Hier kannst du einstellen, wer deine Profilinformationen, wie Bild oder " "Anzeigename einsehen kann." -#: snikket_web/templates/user_profile.html:24 -msgid "Apply" -msgstr "Übernehmen" - diff --git a/snikket_web/translations/en/LC_MESSAGES/messages.po b/snikket_web/translations/en/LC_MESSAGES/messages.po index c7d50af..b7822b7 100644 --- a/snikket_web/translations/en/LC_MESSAGES/messages.po +++ b/snikket_web/translations/en/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\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" "Last-Translator: FULL NAME \n" "Language: en\n" @@ -18,18 +18,74 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.9.0\n" -#: snikket_web/admin.py:44 +#: snikket_web/admin.py:54 msgid "Delete user permanently" 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" msgstr "" -#: snikket_web/admin.py:104 +#: snikket_web/admin.py:170 msgid "Revoke" 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 msgid "Address" msgstr "" @@ -38,7 +94,11 @@ msgstr "" msgid "Password" msgstr "" -#: snikket_web/main.py:60 +#: snikket_web/main.py:43 +msgid "Sign in" +msgstr "" + +#: snikket_web/main.py:64 #, fuzzy msgid "Invalid user name or password." msgstr "" @@ -59,38 +119,100 @@ msgstr "" msgid "The new passwords must match." msgstr "" -#: snikket_web/user.py:50 -msgid "Nobody" -msgstr "" - -#: snikket_web/user.py:51 -msgid "Friends only" +#: snikket_web/user.py:47 +#, fuzzy +msgid "Sign out" msgstr "" #: snikket_web/user.py:52 +msgid "Nobody" +msgstr "" + +#: snikket_web/user.py:53 +msgid "Friends only" +msgstr "" + +#: snikket_web/user.py:54 msgid "Everyone" msgstr "" #: snikket_web/templates/admin_delete_user.html:12 #: 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" msgstr "" -#: snikket_web/user.py:62 +#: snikket_web/user.py:64 msgid "Avatar" msgstr "" -#: snikket_web/user.py:66 +#: snikket_web/user.py:68 msgid "Profile visibility" msgstr "" -#: snikket_web/user.py:91 +#: snikket_web/user.py:97 msgid "Incorrect password" 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_users.html:29 +#: snikket_web/templates/admin_users.html:24 #, python-format msgid "Delete user %(user_name)s" msgstr "" @@ -105,12 +227,12 @@ 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:14 +#: snikket_web/templates/admin_users.html:8 msgid "Login name" msgstr "" #: snikket_web/templates/admin_delete_user.html:14 -#: snikket_web/templates/admin_users.html:16 +#: snikket_web/templates/admin_users.html:10 #, fuzzy msgid "Email address" msgstr "" @@ -119,29 +241,91 @@ msgstr "" msgid "Danger" 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 msgid "View invitation" msgstr "" #: snikket_web/templates/admin_edit_invite.html:13 -#: snikket_web/templates/admin_invites.html:24 -msgid "Created" -msgstr "" - -#: snikket_web/templates/admin_edit_invite.html:15 -#: snikket_web/templates/admin_invites.html:25 +#: snikket_web/templates/admin_invites.html:21 msgid "Valid until" msgstr "" -#: snikket_web/templates/admin_edit_invite.html:17 +#: snikket_web/templates/admin_edit_invite.html:15 msgid "Link" msgstr "" -#: snikket_web/templates/admin_edit_invite.html:24 -#: snikket_web/templates/user_logout.html:12 -#: snikket_web/templates/user_passwd.html:39 -#: snikket_web/templates/user_profile.html:24 -msgid "Back" +#: snikket_web/templates/admin_edit_invite.html:17 +msgid "Reusability" +msgstr "" + +#: 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 "" #: snikket_web/templates/admin_edit_user.html:3 @@ -176,7 +360,7 @@ msgid "At your service, %(user_name)s." msgstr "" #: snikket_web/templates/admin_home.html:7 -#: snikket_web/templates/admin_users.html:10 +#: snikket_web/templates/admin_users.html:4 msgid "Manage users" msgstr "" @@ -184,59 +368,56 @@ msgstr "" msgid "Modify administrative user information or delete users." msgstr "" -#: snikket_web/templates/admin_home.html:11 -#: snikket_web/templates/admin_invites.html:7 +#: snikket_web/templates/admin_home.html:14 +#: snikket_web/templates/admin_invites.html:8 msgid "Manage invitations" msgstr "" -#: snikket_web/templates/admin_home.html:12 +#: snikket_web/templates/admin_home.html:15 msgid "Create, revoke or view invitations." msgstr "" -#: snikket_web/templates/admin_home.html:15 +#: snikket_web/templates/admin_home.html:18 msgid "Back to the main view" msgstr "" -#: snikket_web/templates/admin_home.html:16 +#: snikket_web/templates/admin_home.html:19 msgid "Go back to your user’s web portal page." msgstr "" #: 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" msgstr "" -#: snikket_web/templates/admin_invites.html:26 -#: snikket_web/templates/admin_users.html:18 -msgid "Actions" +#: snikket_web/templates/admin_invites.html:22 +msgid "Reusable" 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" msgstr "" -#: snikket_web/templates/admin_invites.html:38 +#: snikket_web/templates/admin_invites.html:47 msgid "Copy invite link to clipboard" msgstr "" -#: snikket_web/templates/admin_invites.html:40 +#: snikket_web/templates/admin_invites.html:50 msgid "Delete invitation" msgstr "" -#: snikket_web/templates/admin_invites.html:48 +#: snikket_web/templates/admin_invites.html:58 msgid "Currently, there are no pending invitations." msgstr "" -#: snikket_web/templates/admin_users.html:17 +#: snikket_web/templates/admin_users.html:11 msgid "Phone number" msgstr "" @@ -249,13 +430,20 @@ msgstr "" msgid "A Snikket server" 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" msgstr "" -#: snikket_web/templates/library.j2:15 -#, python-format -msgid "Copy "%(content)s" to clipboard" +#: snikket_web/templates/library.j2:79 +msgid "Invalid input" msgstr "" #: snikket_web/templates/login.html:5 @@ -270,10 +458,6 @@ msgstr "" msgid "Login failed" msgstr "" -#: snikket_web/templates/login.html:31 -msgid "Log in" -msgstr "" - #: snikket_web/templates/user_home.html:3 msgid "Welcome!" msgstr "" @@ -294,7 +478,7 @@ msgid "" msgstr "" #: snikket_web/templates/user_home.html:11 -#: snikket_web/templates/user_passwd.html:40 +#: snikket_web/templates/user_passwd.html:42 msgid "Change password" msgstr "" @@ -314,60 +498,58 @@ msgstr "" msgid "Exit the Snikket Web Portal, without logging out your other devices." msgstr "" -#: snikket_web/templates/user_logout.html:7 +#: snikket_web/templates/user_logout.html:8 msgid "Sign out of the Snikket Web Portal" msgstr "" -#: snikket_web/templates/user_logout.html:8 +#: snikket_web/templates/user_logout.html:9 msgid "" "Click below to log yourself out of the web portal. This does not affect " "any other connected devices." msgstr "" -#: snikket_web/templates/user_logout.html:14 -#, fuzzy -msgid "Sign out" -msgstr "" - -#: snikket_web/templates/user_passwd.html:7 +#: snikket_web/templates/user_passwd.html:8 msgid "Change your password" msgstr "" -#: snikket_web/templates/user_passwd.html:8 +#: snikket_web/templates/user_passwd.html:9 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:12 +#: snikket_web/templates/user_passwd.html:13 msgid "Password change failed" msgstr "" -#: snikket_web/templates/user_passwd.html:35 +#: snikket_web/templates/user_passwd.html:36 msgid "Warning" msgstr "" -#: snikket_web/templates/user_passwd.html:36 +#: snikket_web/templates/user_passwd.html:37 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:8 msgid "Profile" msgstr "" -#: snikket_web/templates/user_profile.html:17 +#: snikket_web/templates/user_profile.html:18 msgid "Visibility" msgstr "" -#: snikket_web/templates/user_profile.html:18 +#: snikket_web/templates/user_profile.html:19 msgid "" "This section allows you to control who can see your profile information, " "like avatar and nickname." msgstr "" -#: snikket_web/templates/user_profile.html:24 -msgid "Apply" -msgstr "" +#~ msgid "Copy "%(content)s" to clipboard" +#~ msgstr "" + +#~ msgid "Log in" +#~ msgstr "" + diff --git a/snikket_web/user.py b/snikket_web/user.py index 20db824..98c7133 100644 --- a/snikket_web/user.py +++ b/snikket_web/user.py @@ -43,7 +43,9 @@ class ChangePasswordForm(flask_wtf.FlaskForm): # type:ignore class LogoutForm(flask_wtf.FlaskForm): # type:ignore - pass + action_signout = wtforms.SubmitField( + _l("Sign out"), + ) _ACCESS_MODEL_CHOICES = [ @@ -106,6 +108,9 @@ async def profile() -> typing.Union[str, quart.Response]: form = ProfileForm() if request.method != "POST": 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: profile_access_model = await client.get_nickname_access_model() except quart.exceptions.NotFound: