Merge pull request #76 from snikket-im/feature/roles

"Edit user" flow, role management
This commit is contained in:
Jonas Schäfer
2021-03-25 17:35:42 +01:00
committed by GitHub
15 changed files with 435 additions and 114 deletions

View File

@@ -35,7 +35,6 @@ async def index() -> str:
class PasswordResetLinkPost(BaseForm): class PasswordResetLinkPost(BaseForm):
action_create = wtforms.StringField()
action_revoke = wtforms.StringField() action_revoke = wtforms.StringField()
@@ -57,6 +56,87 @@ async def users() -> str:
) )
_LIMITED_ROLE_NAME = _("Limited")
class EditUserForm(BaseForm):
localpart = wtforms.StringField(
_l("Login name"),
)
display_name = wtforms.StringField(
_l("Display name"),
)
role = wtforms.RadioField(
_l("Access Level"),
choices=[
# NOTE: enable this only after something has been done which
# actually enforces the described restrictions :).
# ("prosody:restricted", _l("Limited")),
("prosody:normal", _l("Normal user")),
("prosody:admin", _l("Administrator")),
],
)
action_save = wtforms.SubmitField(
_l("Update user"),
)
action_create_reset = wtforms.SubmitField(
_l("Create password reset link"),
)
@bp.route("/user/<localpart>/", methods=["GET", "POST"])
@client.require_admin_session()
async def edit_user(localpart: str) -> typing.Union[quart.Response, str]:
target_user_info = await client.get_user_by_localpart(localpart)
form = EditUserForm()
if form.validate_on_submit():
if form.action_create_reset.data:
target_user_info = await client.get_user_by_localpart(localpart)
reset_link = await client.create_password_reset_invite(
localpart=localpart,
ttl=86400,
)
await flash(
_("Password reset link created"),
"success",
)
return redirect(url_for(
".user_password_reset_link",
id_=reset_link.id_,
))
await client.update_user(
localpart,
display_name=form.display_name.data,
roles=[form.role.data],
)
await flash(
_("User information updated."),
"success",
)
return redirect(url_for(".edit_user", localpart=localpart))
elif request.method == "GET":
form.localpart.data = target_user_info.localpart
form.display_name.data = target_user_info.display_name
if target_user_info.roles:
form.role.data = target_user_info.roles[0]
else:
form.role.data = "prosody:normal"
return await render_template(
"admin_edit_user.html",
target_user=target_user_info,
form=form,
)
class DeleteUserForm(BaseForm): class DeleteUserForm(BaseForm):
action_delete = wtforms.SubmitField( action_delete = wtforms.SubmitField(
_l("Delete user permanently") _l("Delete user permanently")
@@ -100,36 +180,38 @@ async def debug_user(localpart: str) -> typing.Union[str, quart.Response]:
) )
@bp.route("/users/password-reset/-", methods=["POST"]) @bp.route("/users/password-reset/<id_>", methods=["GET", "POST"])
@client.require_admin_session() @client.require_admin_session()
async def create_password_reset_link() -> typing.Union[str, quart.Response]: async def user_password_reset_link(
form = PasswordResetLinkPost() id_: str,
if not form.validate_on_submit(): ) -> typing.Union[str, quart.Response]:
abort(400) invite_info = await client.get_invite_by_id(
id_,
if form.action_create.data: )
localpart = form.action_create.data if invite_info.jid is None:
target_user_info = await client.get_user_by_localpart(localpart)
reset_link = await client.create_password_reset_invite(
localpart=localpart,
ttl=86400,
)
await flash( await flash(
_("Password reset link created"), _("Password reset link not found"),
"success", "alert",
)
elif form.action_revoke.data:
await client.delete_invite(form.action_revoke.data)
await flash(
_("Password reset link deleted"),
"success",
) )
return redirect(url_for(".users")) return redirect(url_for(".users"))
localpart = prosodyclient.split_jid(invite_info.jid)[0]
form = PasswordResetLinkPost()
if form.validate_on_submit():
if form.action_revoke.data:
await client.delete_invite(id_)
await flash(
_("Password reset link deleted"),
"success",
)
return redirect(url_for(".edit_user", localpart=localpart))
abort(400)
return await render_template( return await render_template(
"admin_reset_user_password.html", "admin_reset_user_password.html",
target_user=target_user_info, localpart=localpart,
reset_link=reset_link, reset_link=invite_info,
form=form, form=form,
) )

View File

@@ -44,6 +44,15 @@ class AdminUserInfo:
display_name: typing.Optional[str] display_name: typing.Optional[str]
email: typing.Optional[str] email: typing.Optional[str]
phone: typing.Optional[str] phone: typing.Optional[str]
roles: typing.Optional[typing.List[str]]
@property
def has_admin_role(self) -> bool:
return bool(self.roles and "prosody:admin" in self.roles)
@property
def has_restricted_role(self) -> bool:
return bool(self.roles and "prosody:restricted" in self.roles)
@classmethod @classmethod
def from_api_response( def from_api_response(
@@ -55,6 +64,7 @@ class AdminUserInfo:
display_name=data.get("display_name") or None, display_name=data.get("display_name") or None,
email=data.get("email") or None, email=data.get("email") or None,
phone=data.get("phone") or None, phone=data.get("phone") or None,
roles=data.get("roles"),
) )
@@ -858,6 +868,29 @@ class ProsodyClient:
self._raise_error_from_response(resp) self._raise_error_from_response(resp)
return AdminUserInfo.from_api_response(await resp.json()) return AdminUserInfo.from_api_response(await resp.json())
@autosession
async def update_user(
self,
localpart: str,
*,
display_name: typing.Optional[str],
roles: typing.Optional[typing.Collection[str]],
session: aiohttp.ClientSession,
) -> None:
payload: typing.Dict[str, typing.Any] = {
"username": localpart,
}
if display_name is not None:
payload["display_name"] = display_name
if roles is not None:
payload["roles"] = list(roles)
async with session.put(
self._admin_v1_endpoint("/users/{}".format(localpart)),
json=payload,
) as resp:
self._raise_error_from_response(resp)
@autosession @autosession
async def get_user_debug_info( async def get_user_debug_info(
self, self,

View File

@@ -354,6 +354,15 @@ div.form.layout-expanded {
display: block; display: block;
} }
.radio-button-ext > label > p {
margin-left: 1.75rem;
margin-top: 0;
}
.radio-button-ext > label .icon {
margin-left: 0.25em;
}
div.select-wrap { div.select-wrap {
display: block; display: block;
border-bottom: $w-s4 solid $primary-500; border-bottom: $w-s4 solid $primary-500;

View File

@@ -37,6 +37,11 @@ licensed under the terms of the Apache 2.0 License -->
<path d="M0 0h24v24H0V0z" fill="none" /> <path d="M0 0h24v24H0V0z" fill="none" />
<path d="M10.79 16.29c.39.39 1.02.39 1.41 0l3.59-3.59c.39-.39.39-1.02 0-1.41L12.2 7.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41L12.67 11H4c-.55 0-1 .45-1 1s.45 1 1 1h8.67l-1.88 1.88c-.39.39-.38 1.03 0 1.41zM19 3H5c-1.11 0-2 .9-2 2v3c0 .55.45 1 1 1s1-.45 1-1V6c0-.55.45-1 1-1h12c.55 0 1 .45 1 1v12c0 .55-.45 1-1 1H6c-.55 0-1-.45-1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1v3c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z" /> <path d="M10.79 16.29c.39.39 1.02.39 1.41 0l3.59-3.59c.39-.39.39-1.02 0-1.41L12.2 7.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41L12.67 11H4c-.55 0-1 .45-1 1s.45 1 1 1h8.67l-1.88 1.88c-.39.39-.38 1.03 0 1.41zM19 3H5c-1.11 0-2 .9-2 2v3c0 .55.45 1 1 1s1-.45 1-1V6c0-.55.45-1 1-1h12c.55 0 1 .45 1 1v12c0 .55-.45 1-1 1H6c-.55 0-1-.45-1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1v3c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z" />
</symbol> </symbol>
<!-- from: action/lock/materialiconsround/24px.svg -->
<symbol id="icon-lock" viewBox="0 0 24 24">
<g fill="none"><path d="M0 0h24v24H0V0z" /><path d="M0 0h24v24H0V0z" opacity=".87" /></g>
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zM9 8V6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9z" />
</symbol>
<!-- from: communication/qr_code/materialiconsround/24px.svg --> <!-- from: communication/qr_code/materialiconsround/24px.svg -->
<symbol id="icon-qrcode" viewBox="0 0 24 24"> <symbol id="icon-qrcode" viewBox="0 0 24 24">
<g><rect fill="none" height="24" width="24" /><rect fill="none" height="24" width="24" /></g> <g><rect fill="none" height="24" width="24" /><rect fill="none" height="24" width="24" /></g>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -16,7 +16,7 @@
<p>{% trans %}The user and their data will be deleted irrevocably, permanently and immediately upon pushing the below button. <strong>There is no way back!</strong>{% endtrans %}</p> <p>{% trans %}The user and their data will be deleted irrevocably, permanently and immediately upon pushing the below button. <strong>There is no way back!</strong>{% endtrans %}</p>
{% endcall %} {% endcall %}
<div class="f-bbox"> <div class="f-bbox">
{%- call standard_button("back", url_for(".index"), class="secondary") %}{% trans %}Back{% endtrans %}{% endcall -%} {%- call standard_button("back", url_for(".edit_user", localpart=target_user.localpart), class="tertiary") %}{% trans %}Back{% endtrans %}{% endcall -%}
{%- call form_button("delete", form.action_delete, class="primary danger") %}{% endcall -%} {%- call form_button("delete", form.action_delete, class="primary danger") %}{% endcall -%}
</div> </div>
</form></div> </form></div>

View File

@@ -40,7 +40,7 @@
{%- endif -%} {%- endif -%}
</div> </div>
<div class="f-bbox"> <div class="f-bbox">
{%- call standard_button("back", url_for(".circles"), class="secondary") -%} {%- call standard_button("back", url_for(".circles"), class="tertiary") -%}
{% trans %}Return to circle list{% endtrans %} {% trans %}Return to circle list{% endtrans %}
{%- endcall -%} {%- endcall -%}
{%- call form_button("done", form.action_save, class="primary") %}{% endcall -%} {%- call form_button("done", form.action_save, class="primary") %}{% endcall -%}

View File

@@ -44,10 +44,10 @@
<dd>{{ invite.created_at | format_date }}</dd> <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 standard_button("back", url_for(".invitations"), class="tertiary") %}
{%- call standard_button("back", url_for(".invitations"), class="primary") %}
{% trans %}Return to invitation list{% endtrans %} {% trans %}Return to invitation list{% endtrans %}
{%- endcall %} {%- endcall %}
{%- call form_button("remove_link", form.action_revoke, class="primary danger") %}{% endcall -%}
</div> </div>
</div> </div>
</form> </form>

View File

@@ -0,0 +1,79 @@
{% extends "admin_app.html" %}
{% from "library.j2" import box, form_button, standard_button, icon %}
{% macro access_level_description(role, caller=None) %}
{%- if role == "prosody:restricted" -%}
{% trans %}Limited users can interact with users on the same Snikket service and be members of circles.{% endtrans %}
{%- elif role == "prosody:normal" -%}
{% trans %}Like limited users and can also interact with users on other Snikket services.{% endtrans %}
{%- elif role == "prosody:admin" -%}
{% trans %}Like normal users and can access the admin panel in the web portal.{% endtrans %}
{%- endif -%}
{% endmacro %}
{% macro access_level_icon(role, caller=None) %}
{%- if role == "prosody:restricted" -%}
{% call icon("lock") %}{% endcall %}
{%- elif role == "prosody:admin" -%}
{% call icon("admin") %}{% endcall %}
{%- endif -%}
{% endmacro %}
{% block content %}
<h1>{% trans user_name=target_user.localpart %}Edit user {{ user_name }}{% endtrans %}</h1>
<div class="form layout-expanded"><form method="POST">
{{ form.csrf_token }}
<h2 class="form-title">{% trans %}Edit user{% endtrans %}</h2>
<div class="f-ebox">
{{ form.localpart.label }}
{{ form.localpart(readonly="readonly") }}
<p class="form-desc weak">{% trans %}The login name cannot be changed.{% endtrans %}</p>
</div>
<div class="f-ebox">
{{ form.display_name.label }}
{{ form.display_name }}
</div>
<h3 class="form-title">{% trans %}Access Level{% endtrans %}</h3>
<p class="form-descr weak">{% trans %}The access level of a user determines what interactions are allowed for them on your Snikket service.{% endtrans %}</p>
<div class="f-ebox">
<fieldset>{#- -#}
<legend class="a11y-only">{{ form.role.label.text }}</legend>
{%- for level in form.role -%}
<div class="radio-button-ext">
{{ level }}<label for="{{ level.id }}">
{%- trans title=level.label.text, icon=access_level_icon(level.data), description=access_level_description(level.data) -%}
<strong>{{ title }}{{ icon }}</strong><p>{{ description }}</p>
{%- endtrans -%}
</label>
</div>
{%- endfor -%}
</fieldset>
</div>
<div class="f-bbox">
{%- call standard_button("back", url_for(".users"), class="tertiary") -%}
{%- trans -%}Return to user list{%- endtrans -%}
{%- endcall -%}
{%- call standard_button("delete", url_for(".delete_user", localpart=target_user.localpart), class="secondary") -%}
{%- trans -%}Delete user{%- endtrans -%}
{%- endcall -%}
{%- call form_button("done", form.action_save, class="primary") %}{% endcall -%}
</div>
</form></div>
<h2>{% trans %}Further actions{% endtrans %}</h2>
<div class="form layout-expanded"><form method="POST">
<h2 class="form-title">{% trans %}Reset password{% endtrans %}</h2>
{{ form.csrf_token }}
<p class="form-desc">
{% trans %}If the user has lost their password, you can use the button below to create a special link which allows to change the password of the account, once.{% endtrans %}
</p>
<div class="f-bbox">
{%- call form_button("passwd", form.action_create_reset, class="primary") -%}{%- endcall -%}
</div>
<h2 class="form-title">{% trans %}Debug information{% endtrans %}</h2>
<p class="form-desc">
{% trans %}In some cases, extended information about the user account and the connected devices is necessary to troubleshoot issues. The button below reveals this (sensitive) information.{% endtrans %}
</p>
<div class="f-bbox">
{%- call standard_button("bug_report", url_for(".debug_user", localpart=target_user.localpart), class="primary") -%}
{%- trans -%}Show debug information{%- endtrans -%}
{%- endcall -%}
</div>
</form></div>
{% endblock %}

View File

@@ -9,7 +9,7 @@
<form method="POST"> <form method="POST">
{{- form.csrf_token -}} {{- form.csrf_token -}}
<div class="form layout-expanded"> <div class="form layout-expanded">
<h2 class="form-title">{% trans user_name=target_user.localpart %}Password reset link for {{ user_name }}{% endtrans %}</h2> <h2 class="form-title">{% trans user_name=localpart %}Password reset link for {{ user_name }}{% endtrans %}</h2>
<p class="form-desc">{% trans %}The following link will allow the user to reset their password on their device, once.{% endtrans %}</p> <p class="form-desc">{% trans %}The following link will allow the user to reset their password on their device, once.{% endtrans %}</p>
<dd> <dd>
<dt>{% trans %}Valid until{% endtrans %}</dt> <dt>{% trans %}Valid until{% endtrans %}</dt>
@@ -21,7 +21,7 @@
{%- call custom_form_button("remove_link", form.action_revoke.name, reset_link.id_, class="secondary danger") -%} {%- call custom_form_button("remove_link", form.action_revoke.name, reset_link.id_, class="secondary danger") -%}
{% trans %}Destroy link{% endtrans %} {% trans %}Destroy link{% endtrans %}
{%- endcall -%} {%- endcall -%}
{%- call standard_button("back", url_for(".users"), class="primary") -%} {%- call standard_button("back", url_for(".edit_user", localpart=localpart), class="primary") -%}
{% trans %}Back{% endtrans %} {% trans %}Back{% endtrans %}
{%- endcall -%} {%- endcall -%}
</div> </div>

View File

@@ -1,9 +1,7 @@
{% extends "admin_app.html" %} {% extends "admin_app.html" %}
{% from "library.j2" import action_button, value_or_hint, custom_form_button %} {% from "library.j2" import action_button, icon, value_or_hint, custom_form_button %}
{% block content %} {% block content %}
<h1>{% trans %}Manage users{% endtrans %}</h1> <h1>{% trans %}Manage users{% endtrans %}</h1>
<form method="POST" action="{{ url_for(".create_password_reset_link") }}">
{{- reset_form.csrf_token -}}
<div class="elevated el-2"><table> <div class="elevated el-2"><table>
<thead> <thead>
<tr> <tr>
@@ -15,17 +13,19 @@
<tbody> <tbody>
{% for user in users %} {% for user in users %}
<tr> <tr>
<td>{{ user.localpart }}</td> <td>
{{- user.localpart -}}
{%- if user.has_admin_role -%}
<span class="with-tooltip above" data-tooltip="{% trans %}The user is an administrator.{% endtrans %}">{% call icon("admin") %}{% trans %} (Administrator){% endtrans %}{% endcall %}</span>
{%- endif -%}
{%- if user.has_restricted_role -%}
<span class="with-tooltip above" data-tooltip="{% trans %}The user is restricted.{% endtrans %}">{% call icon("lock") %}{% trans %} (Restricted){% endtrans %}{% endcall %}</span>
{%- endif -%}
</td>
<td>{% call value_or_hint(user.display_name) %}{% endcall %}</td> <td>{% call value_or_hint(user.display_name) %}{% endcall %}</td>
<td class="nowrap"> <td class="nowrap">
{%- call action_button("delete", url_for(".delete_user", localpart=user.localpart), class="secondary") -%} {%- call action_button("edit", url_for(".edit_user", localpart=user.localpart), class="primary") -%}
{% trans user_name=user.localpart %}Delete user {{ user_name }}{% endtrans %} {% trans user_name=user.localpart %}Edit user {{ user_name }}{% endtrans %}
{%- endcall -%}
{%- call action_button("bug_report", url_for(".debug_user", localpart=user.localpart), class="secondary") -%}
{% trans user_name=user.localpart %}Show debug information for {{ user_name }}{% endtrans %}
{%- endcall -%}
{%- call custom_form_button("passwd", reset_form.action_create.name, user.localpart, class="secondary", slim=True) -%}
{% trans user_name=user.localpart %}Create password reset link for {{ user_name }}{% endtrans %}
{%- endcall -%} {%- endcall -%}
</form> </form>
</td> </td>
@@ -33,6 +33,5 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table></div> </table></div>
</form>
{%- include "admin_create_invite_form.html" -%} {%- include "admin_create_invite_form.html" -%}
{% endblock %} {% endblock %}

View File

@@ -6,7 +6,7 @@
<p class="form-desc">{% trans %}Click below to log yourself out of the web portal. This does not affect any other connected devices.{% endtrans %}</p> <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") -%} {%- call standard_button("back", url_for("user.index"), class="tertiary") -%}
{% trans %}Back{% endtrans %} {% trans %}Back{% endtrans %}
{%- endcall -%} {%- endcall -%}
{%- call form_button("logout", form.action_signout, class="primary") %}{% endcall -%} {%- call form_button("logout", form.action_signout, class="primary") %}{% endcall -%}

View File

@@ -24,7 +24,7 @@
<p>{% trans %}After changing your password, you will have to enter the new password on all of your devices.{% endtrans %}</p> <p>{% trans %}After changing your password, you will have to enter the new password on all of your devices.{% endtrans %}</p>
</div> </div>
<div class="f-bbox"> <div class="f-bbox">
{%- call standard_button("back", url_for('.index'), class="secondary") %}{% trans %}Back{% endtrans %}{% endcall -%} {%- call standard_button("back", url_for('.index'), class="tertiary") %}{% trans %}Back{% endtrans %}{% endcall -%}
{%- call custom_form_button("passwd", "", "", class="primary") -%} {%- call custom_form_button("passwd", "", "", class="primary") -%}
{% trans %}Change password{% endtrans %} {% trans %}Change password{% endtrans %}
{%- endcall -%} {%- endcall -%}

View File

@@ -29,7 +29,7 @@
</fieldset> </fieldset>
</div> </div>
<div class="f-bbox"> <div class="f-bbox">
{%- call standard_button("back", url_for('.index'), class="secondary") %}{% trans %}Back{% endtrans %}{% endcall -%} {%- call standard_button("back", url_for('.index'), class="tertiary") %}{% trans %}Back{% endtrans %}{% endcall -%}
{%- call form_button("done", form.action_save, class="primary") %}{% endcall -%} {%- call form_button("done", form.action_save, class="primary") %}{% endcall -%}
</div> </div>
<script type="text/javascript"> <script type="text/javascript">

View File

@@ -8,7 +8,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-03-22 15:08+0100\n" "POT-Creation-Date: 2021-03-25 17:32+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -18,130 +18,172 @@ msgstr ""
"Generated-By: Babel 2.9.0\n" "Generated-By: Babel 2.9.0\n"
#: snikket_web/admin.py:59 #: snikket_web/admin.py:59
msgid "Delete user permanently" msgid "Limited"
msgstr "" msgstr ""
#: snikket_web/admin.py:72 #: snikket_web/admin.py:64 snikket_web/templates/admin_delete_user.html:10
msgid "User deleted" #: snikket_web/templates/admin_users.html:8
msgid "Login name"
msgstr "" msgstr ""
#: snikket_web/admin.py:115 #: snikket_web/admin.py:68 snikket_web/templates/admin_delete_user.html:12
#: snikket_web/templates/admin_users.html:9 snikket_web/user.py:61
msgid "Display name"
msgstr ""
#: snikket_web/admin.py:72 snikket_web/templates/admin_edit_user.html:33
msgid "Access Level"
msgstr ""
#: snikket_web/admin.py:77
msgid "Normal user"
msgstr ""
#: snikket_web/admin.py:78
msgid "Administrator"
msgstr ""
#: snikket_web/admin.py:83
msgid "Update user"
msgstr ""
#: snikket_web/admin.py:87
msgid "Create password reset link"
msgstr ""
#: snikket_web/admin.py:105
msgid "Password reset link created" msgid "Password reset link created"
msgstr "" msgstr ""
#: snikket_web/admin.py:121 #: snikket_web/admin.py:120
msgid "Password reset link deleted" msgid "User information updated."
msgstr "" msgstr ""
#: snikket_web/admin.py:140 #: snikket_web/admin.py:142
msgid "Invite to circle" msgid "Delete user permanently"
msgstr ""
#: snikket_web/admin.py:146
msgid "At least one circle must be selected"
msgstr ""
#: snikket_web/admin.py:151
msgid "Valid for"
msgstr ""
#: snikket_web/admin.py:153
msgid "One hour"
msgstr ""
#: snikket_web/admin.py:154
msgid "Twelve hours"
msgstr "" msgstr ""
#: snikket_web/admin.py:155 #: snikket_web/admin.py:155
msgid "User deleted"
msgstr ""
#: snikket_web/admin.py:193
msgid "Password reset link not found"
msgstr ""
#: snikket_web/admin.py:205
msgid "Password reset link deleted"
msgstr ""
#: snikket_web/admin.py:225
msgid "Invite to circle"
msgstr ""
#: snikket_web/admin.py:231
msgid "At least one circle must be selected"
msgstr ""
#: snikket_web/admin.py:236
msgid "Valid for"
msgstr ""
#: snikket_web/admin.py:238
msgid "One hour"
msgstr ""
#: snikket_web/admin.py:239
msgid "Twelve hours"
msgstr ""
#: snikket_web/admin.py:240
msgid "One day" msgid "One day"
msgstr "" msgstr ""
#: snikket_web/admin.py:156 #: snikket_web/admin.py:241
msgid "One week" msgid "One week"
msgstr "" msgstr ""
#: snikket_web/admin.py:157 #: snikket_web/admin.py:242
msgid "Four weeks" msgid "Four weeks"
msgstr "" msgstr ""
#: snikket_web/admin.py:163 snikket_web/templates/admin_edit_invite.html:17 #: snikket_web/admin.py:248 snikket_web/templates/admin_edit_invite.html:17
msgid "Invitation type" msgid "Invitation type"
msgstr "" msgstr ""
#: snikket_web/admin.py:165 snikket_web/templates/library.j2:116 #: snikket_web/admin.py:250 snikket_web/templates/library.j2:116
msgid "Individual" msgid "Individual"
msgstr "" msgstr ""
#: snikket_web/admin.py:166 snikket_web/templates/library.j2:114 #: snikket_web/admin.py:251 snikket_web/templates/library.j2:114
msgid "Group" msgid "Group"
msgstr "" msgstr ""
#: snikket_web/admin.py:172 #: snikket_web/admin.py:257
msgid "New invitation link" msgid "New invitation link"
msgstr "" msgstr ""
#: snikket_web/admin.py:234 #: snikket_web/admin.py:319
msgid "Revoke" msgid "Revoke"
msgstr "" msgstr ""
#: snikket_web/admin.py:258 #: snikket_web/admin.py:343
msgid "Invitation created" msgid "Invitation created"
msgstr "" msgstr ""
#: snikket_web/admin.py:274 #: snikket_web/admin.py:359
msgid "No such invitation exists" msgid "No such invitation exists"
msgstr "" msgstr ""
#: snikket_web/admin.py:289 #: snikket_web/admin.py:374
msgid "Invitation revoked" msgid "Invitation revoked"
msgstr "" msgstr ""
#: snikket_web/admin.py:306 snikket_web/admin.py:354 #: snikket_web/admin.py:391 snikket_web/admin.py:439
msgid "Name" msgid "Name"
msgstr "" msgstr ""
#: snikket_web/admin.py:311 snikket_web/templates/admin_circles.html:47 #: snikket_web/admin.py:396 snikket_web/templates/admin_circles.html:47
msgid "Create circle" msgid "Create circle"
msgstr "" msgstr ""
#: snikket_web/admin.py:341 #: snikket_web/admin.py:426
msgid "Circle created" msgid "Circle created"
msgstr "" msgstr ""
#: snikket_web/admin.py:359 #: snikket_web/admin.py:444
msgid "Select user" msgid "Select user"
msgstr "" msgstr ""
#: snikket_web/admin.py:364 #: snikket_web/admin.py:449
msgid "Update circle" msgid "Update circle"
msgstr "" msgstr ""
#: snikket_web/admin.py:368 #: snikket_web/admin.py:453
msgid "Delete circle permanently" msgid "Delete circle permanently"
msgstr "" msgstr ""
#: snikket_web/admin.py:374 #: snikket_web/admin.py:459
msgid "Add user" msgid "Add user"
msgstr "" msgstr ""
#: snikket_web/admin.py:390 #: snikket_web/admin.py:475
msgid "No such circle exists" msgid "No such circle exists"
msgstr "" msgstr ""
#: snikket_web/admin.py:427 #: snikket_web/admin.py:512
msgid "Circle data updated" msgid "Circle data updated"
msgstr "" msgstr ""
#: snikket_web/admin.py:433 #: snikket_web/admin.py:518
msgid "Circle deleted" msgid "Circle deleted"
msgstr "" msgstr ""
#: snikket_web/admin.py:444 #: snikket_web/admin.py:529
msgid "User added to circle" msgid "User added to circle"
msgstr "" msgstr ""
#: snikket_web/admin.py:453 #: snikket_web/admin.py:538
msgid "User removed from circle" msgid "User removed from circle"
msgstr "" msgstr ""
@@ -234,11 +276,6 @@ msgstr ""
msgid "Everyone" msgid "Everyone"
msgstr "" msgstr ""
#: snikket_web/templates/admin_delete_user.html:12
#: snikket_web/templates/admin_users.html:11 snikket_web/user.py:61
msgid "Display name"
msgstr ""
#: snikket_web/user.py:65 #: snikket_web/user.py:65
msgid "Avatar" msgid "Avatar"
msgstr "" msgstr ""
@@ -384,7 +421,7 @@ msgstr ""
#: snikket_web/templates/admin_circles.html:15 #: snikket_web/templates/admin_circles.html:15
#: snikket_web/templates/admin_invites.html:24 #: snikket_web/templates/admin_invites.html:24
#: snikket_web/templates/admin_users.html:12 #: snikket_web/templates/admin_users.html:10
msgid "Actions" msgid "Actions"
msgstr "" msgstr ""
@@ -455,12 +492,12 @@ msgid "Copy complete output"
msgstr "" msgstr ""
#: snikket_web/templates/admin_delete_user.html:4 #: snikket_web/templates/admin_delete_user.html:4
#: snikket_web/templates/admin_users.html:22
#, python-format #, python-format
msgid "Delete user %(user_name)s" msgid "Delete user %(user_name)s"
msgstr "" msgstr ""
#: snikket_web/templates/admin_delete_user.html:6 #: snikket_web/templates/admin_delete_user.html:6
#: snikket_web/templates/admin_edit_user.html:54
msgid "Delete user" msgid "Delete user"
msgstr "" msgstr ""
@@ -468,11 +505,6 @@ msgstr ""
msgid "Are you sure you want to delete the following user?" 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_users.html:10
msgid "Login name"
msgstr ""
#: snikket_web/templates/admin_delete_user.html:15 #: snikket_web/templates/admin_delete_user.html:15
msgid "Danger" msgid "Danger"
msgstr "" msgstr ""
@@ -606,10 +638,85 @@ msgstr ""
msgid "Created" msgid "Created"
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_invite.html:49 #: snikket_web/templates/admin_edit_invite.html:48
msgid "Return to invitation list" msgid "Return to invitation list"
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_user.html:5
msgid ""
"Limited users can interact with users on the same Snikket service and be "
"members of circles."
msgstr ""
#: snikket_web/templates/admin_edit_user.html:7
msgid ""
"Like limited users and can also interact with users on other Snikket "
"services."
msgstr ""
#: snikket_web/templates/admin_edit_user.html:9
msgid "Like normal users and can access the admin panel in the web portal."
msgstr ""
#: snikket_web/templates/admin_edit_user.html:20
#: snikket_web/templates/admin_users.html:28
#, python-format
msgid "Edit user %(user_name)s"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:23
msgid "Edit user"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:27
msgid "The login name cannot be changed."
msgstr ""
#: snikket_web/templates/admin_edit_user.html:34
msgid ""
"The access level of a user determines what interactions are allowed for "
"them on your Snikket service."
msgstr ""
#: snikket_web/templates/admin_edit_user.html:41
#, python-format
msgid "<strong>%(title)s%(icon)s</strong><p>%(description)s</p>"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:51
msgid "Return to user list"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:59
msgid "Further actions"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:61
msgid "Reset password"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:64
msgid ""
"If the user has lost their password, you can use the button below to "
"create a special link which allows to change the password of the account,"
" once."
msgstr ""
#: snikket_web/templates/admin_edit_user.html:69
msgid "Debug information"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:71
msgid ""
"In some cases, extended information about the user account and the "
"connected devices is necessary to troubleshoot issues. The button below "
"reveals this (sensitive) information."
msgstr ""
#: snikket_web/templates/admin_edit_user.html:75
msgid "Show debug information"
msgstr ""
#: snikket_web/templates/admin_home.html:4 #: snikket_web/templates/admin_home.html:4
msgid "Welcome to the admin panel!" msgid "Welcome to the admin panel!"
msgstr "" msgstr ""
@@ -704,14 +811,20 @@ msgstr ""
msgid "Destroy link" msgid "Destroy link"
msgstr "" msgstr ""
#: snikket_web/templates/admin_users.html:25 #: snikket_web/templates/admin_users.html:19
#, python-format msgid "The user is an administrator."
msgid "Show debug information for %(user_name)s"
msgstr "" msgstr ""
#: snikket_web/templates/admin_users.html:28 #: snikket_web/templates/admin_users.html:19
#, python-format msgid " (Administrator)"
msgid "Create password reset link for %(user_name)s" msgstr ""
#: snikket_web/templates/admin_users.html:22
msgid "The user is restricted."
msgstr ""
#: snikket_web/templates/admin_users.html:22
msgid " (Restricted)"
msgstr "" msgstr ""
#: snikket_web/templates/app.html:4 #: snikket_web/templates/app.html:4
@@ -1054,12 +1167,12 @@ msgstr ""
#: snikket_web/templates/invite_view.html:99 #: snikket_web/templates/invite_view.html:99
msgid "" msgid ""
"After downloading Snikket from the app store, you have to return to this " "After downloading Snikket from the App Store, you have to return to this "
"invite link and tap on \"Open the app\" to proceed." "invite link and tap on \"Open the app\" to proceed."
msgstr "" msgstr ""
#: snikket_web/templates/invite_view.html:101 #: snikket_web/templates/invite_view.html:101
msgid "First download Snikket from the app store using the button below:" msgid "First download Snikket from the App Store using the button below:"
msgstr "" msgstr ""
#: snikket_web/templates/invite_view.html:103 #: snikket_web/templates/invite_view.html:103

View File

@@ -5,6 +5,7 @@ action/delete:delete
action/logout:logout action/logout:logout
action/login:login action/login:login
action/exit_to_app:exit_to_app action/exit_to_app:exit_to_app
action/lock:lock
communication/qr_code:qrcode communication/qr_code:qrcode
communication/vpn_key:passwd communication/vpn_key:passwd
content/add_circle_outline:add content/add_circle_outline:add