You've already forked snikket-web-portal
Compare commits
14 Commits
feature/mu
...
feature/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55b195cd7f | ||
|
|
46a7d0c37d | ||
|
|
c63b95c6e0 | ||
|
|
6848691141 | ||
|
|
1e83881a24 | ||
|
|
35e6bec328 | ||
|
|
d345f0d98d | ||
|
|
f5ccb7d858 | ||
|
|
f7c8bccfa2 | ||
|
|
e5d06877a4 | ||
|
|
e7ed9dd176 | ||
|
|
6778557db8 | ||
|
|
73f3f25515 | ||
|
|
bd66600d05 |
@@ -86,6 +86,14 @@ class EditUserForm(BaseForm):
|
||||
_l("Update user"),
|
||||
)
|
||||
|
||||
action_restore = wtforms.SubmitField(
|
||||
_l("Restore account"),
|
||||
)
|
||||
|
||||
action_enable = wtforms.SubmitField(
|
||||
_l("Unlock account"),
|
||||
)
|
||||
|
||||
action_create_reset = wtforms.SubmitField(
|
||||
_l("Create password reset link"),
|
||||
)
|
||||
@@ -112,6 +120,32 @@ async def edit_user(localpart: str) -> typing.Union[werkzeug.Response, str]:
|
||||
".user_password_reset_link",
|
||||
id_=reset_link.id_,
|
||||
))
|
||||
elif form.action_restore.data or form.action_enable.data:
|
||||
await client.enable_user_account(localpart)
|
||||
try:
|
||||
if form.action_restore.data:
|
||||
await flash(
|
||||
_("User account restored"),
|
||||
"success",
|
||||
)
|
||||
else:
|
||||
await flash(
|
||||
_("User account unlocked"),
|
||||
"success",
|
||||
)
|
||||
return redirect(url_for(".users"))
|
||||
except aiohttp.ClientResponseError:
|
||||
if form.action_restore.data:
|
||||
await flash(
|
||||
_("Could not restore user account"),
|
||||
"alert",
|
||||
)
|
||||
else:
|
||||
await flash(
|
||||
_("Could not unlock user account"),
|
||||
"alert",
|
||||
)
|
||||
return redirect(url_for(".edit_user", localpart=localpart))
|
||||
|
||||
await client.update_user(
|
||||
localpart,
|
||||
@@ -123,7 +157,7 @@ async def edit_user(localpart: str) -> typing.Union[werkzeug.Response, str]:
|
||||
_("User information updated."),
|
||||
"success",
|
||||
)
|
||||
return redirect(url_for(".edit_user", localpart=localpart))
|
||||
return redirect(url_for(".users"))
|
||||
|
||||
elif request.method == "GET":
|
||||
form.localpart.data = target_user_info.localpart
|
||||
|
||||
@@ -4,6 +4,8 @@ import math
|
||||
import secrets
|
||||
import typing
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import quart.flask_patch # noqa:F401
|
||||
from quart import (
|
||||
current_app,
|
||||
@@ -13,7 +15,8 @@ from quart import (
|
||||
|
||||
import flask_babel
|
||||
import flask_wtf
|
||||
from flask_babel import _
|
||||
from flask_babel import lazy_gettext as _l
|
||||
import flask_babel as _
|
||||
|
||||
from . import prosodyclient
|
||||
|
||||
@@ -70,6 +73,43 @@ def format_bytes(n: float) -> str:
|
||||
return "{} {}".format(n, unit)
|
||||
|
||||
|
||||
def format_last_activity(timestamp: typing.Optional[int]) -> str:
|
||||
if timestamp is None:
|
||||
return _l("Never")
|
||||
|
||||
last_active = datetime.fromtimestamp(timestamp, tz=timezone.utc)
|
||||
# TODO: This 'now' should use the user's local time zone, but we
|
||||
# don't have that information. Thus 'today'/'yesterday' may be
|
||||
# slightly inaccurate, but compared to alternative solutions it
|
||||
# should hopefully be "good enough".
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
time_ago = now - last_active
|
||||
|
||||
yesterday = now - timedelta(days=1)
|
||||
|
||||
if (
|
||||
last_active.year == now.year
|
||||
and last_active.month == now.month
|
||||
and last_active.day == now.day
|
||||
):
|
||||
return _l("Today")
|
||||
elif (
|
||||
last_active.year == yesterday.year
|
||||
and last_active.month == yesterday.month
|
||||
and last_active.day == yesterday.day
|
||||
):
|
||||
return _l("Yesterday")
|
||||
|
||||
return _.gettext(
|
||||
"%(time)s ago",
|
||||
time=flask_babel.format_timedelta(time_ago, granularity="day"),
|
||||
)
|
||||
|
||||
|
||||
def template_now() -> typing.Dict[str, typing.Any]:
|
||||
return dict(now=lambda: datetime.now(timezone.utc))
|
||||
|
||||
|
||||
def add_vary_language_header(resp: quart.Response) -> quart.Response:
|
||||
if getattr(g, "language_header_accessed", False):
|
||||
resp.vary.add("Accept-Language")
|
||||
@@ -86,6 +126,8 @@ def init_templating(app: quart.Quart) -> None:
|
||||
app.template_filter("format_bytes")(format_bytes)
|
||||
app.template_filter("flatten")(flatten)
|
||||
app.template_filter("circle_name")(circle_name)
|
||||
app.template_filter("format_last_activity")(format_last_activity)
|
||||
app.context_processor(template_now)
|
||||
app.after_request(add_vary_language_header)
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import types
|
||||
import typing
|
||||
import typing_extensions
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import aiohttp
|
||||
|
||||
@@ -42,6 +42,52 @@ class TokenInfo:
|
||||
scopes: typing.Collection[str]
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class UserDeletionRequestInfo:
|
||||
deleted_at: datetime
|
||||
pending_until: datetime
|
||||
|
||||
@classmethod
|
||||
def from_api_response(
|
||||
cls,
|
||||
data: typing.Optional[typing.Mapping[str, typing.Any]],
|
||||
) -> typing.Optional["UserDeletionRequestInfo"]:
|
||||
if data is None:
|
||||
return None
|
||||
return cls(
|
||||
deleted_at=datetime.fromtimestamp(
|
||||
data["deleted_at"],
|
||||
tz=timezone.utc
|
||||
),
|
||||
pending_until=datetime.fromtimestamp(
|
||||
data["pending_until"],
|
||||
tz=timezone.utc
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class AvatarMetadata:
|
||||
bytes: int
|
||||
hash: str
|
||||
type: str
|
||||
width: typing.Optional[int]
|
||||
height: typing.Optional[int]
|
||||
|
||||
@classmethod
|
||||
def from_api_response(
|
||||
cls,
|
||||
data: typing.Mapping[str, typing.Any],
|
||||
) -> "AvatarMetadata":
|
||||
return cls(
|
||||
hash=data["hash"],
|
||||
bytes=data["bytes"],
|
||||
type=data["type"],
|
||||
width=data.get("width") or None,
|
||||
height=data.get("height") or None,
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class AdminUserInfo:
|
||||
localpart: str
|
||||
@@ -49,6 +95,10 @@ class AdminUserInfo:
|
||||
email: typing.Optional[str]
|
||||
phone: typing.Optional[str]
|
||||
roles: typing.Optional[typing.List[str]]
|
||||
enabled: bool
|
||||
last_active: typing.Optional[int]
|
||||
deletion_request: typing.Optional[UserDeletionRequestInfo]
|
||||
avatar_info: typing.List[AvatarMetadata]
|
||||
|
||||
@property
|
||||
def has_admin_role(self) -> bool:
|
||||
@@ -75,6 +125,15 @@ class AdminUserInfo:
|
||||
email=data.get("email") or None,
|
||||
phone=data.get("phone") or None,
|
||||
roles=roles,
|
||||
enabled=data.get("enabled", True),
|
||||
last_active=data.get("last_active") or None,
|
||||
deletion_request=UserDeletionRequestInfo.from_api_response(
|
||||
data.get("deletion_request")
|
||||
),
|
||||
avatar_info=[
|
||||
AvatarMetadata.from_api_response(avatar_info)
|
||||
for avatar_info in data.get("avatar_info", [])
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -925,6 +984,36 @@ class ProsodyClient:
|
||||
) as resp:
|
||||
self._raise_error_from_response(resp)
|
||||
|
||||
@autosession
|
||||
async def enable_user_account(
|
||||
self,
|
||||
localpart: str,
|
||||
*,
|
||||
session: aiohttp.ClientSession,
|
||||
) -> None:
|
||||
async with session.patch(
|
||||
self._admin_v1_endpoint("/users/{}".format(localpart)),
|
||||
json={
|
||||
"enabled": True,
|
||||
},
|
||||
) as resp:
|
||||
self._raise_error_from_response(resp)
|
||||
|
||||
@autosession
|
||||
async def disable_user_account(
|
||||
self,
|
||||
localpart: str,
|
||||
*,
|
||||
session: aiohttp.ClientSession,
|
||||
) -> None:
|
||||
async with session.patch(
|
||||
self._admin_v1_endpoint("/users/{}".format(localpart)),
|
||||
json={
|
||||
"enabled": False,
|
||||
},
|
||||
) as resp:
|
||||
self._raise_error_from_response(resp)
|
||||
|
||||
@autosession
|
||||
async def get_user_debug_info(
|
||||
self,
|
||||
|
||||
@@ -708,8 +708,7 @@ input[type="submit"], button, .button {
|
||||
height: 1.5em;
|
||||
vertical-align: middle;
|
||||
background-size: cover;
|
||||
box-shadow: inset 0px 0px 0px 2px rgba(0, 0, 0, 0.2);
|
||||
border-radius: $w-s4;
|
||||
border-radius: 10%;
|
||||
|
||||
margin: 0 0.25em;
|
||||
|
||||
@@ -1068,6 +1067,10 @@ pre.guru-meditation {
|
||||
}
|
||||
}
|
||||
|
||||
label, legend {
|
||||
color: $gray-800 !important;
|
||||
}
|
||||
|
||||
.box {
|
||||
background-color: black;
|
||||
border-color: $gray-800;
|
||||
@@ -1202,6 +1205,13 @@ pre.guru-meditation {
|
||||
p.form-desc.weak, p.field-desc.weak {
|
||||
color: $gray-700;
|
||||
}
|
||||
|
||||
.user-badge-icon {
|
||||
color: $gray-900 !important;
|
||||
background-color: $gray-100 !important;
|
||||
border-color: $gray-300 !important;
|
||||
box-shadow: black 0 0 2px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* tooltip magic */
|
||||
@@ -1252,3 +1262,46 @@ pre.guru-meditation {
|
||||
.with-tooltip:hover:before, .with-tooltip:hover:after {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.username-with-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.avatar-container {
|
||||
position: relative;
|
||||
|
||||
.avatar {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.user-badge-icon {
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
right: 0px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
border-color: $gray-500;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-shadow: $gray-500 0px 0px 2px;
|
||||
|
||||
line-height: 1;
|
||||
.icon {
|
||||
/* vertical-align: text-bottom; */
|
||||
padding: 0.1em;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info-container {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -42,6 +42,16 @@ licensed under the terms of the Apache 2.0 License -->
|
||||
<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: action/lock_open/materialiconsround/24px.svg -->
|
||||
<symbol id="icon-lock_open" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M12 13c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6-5h-1V6c0-2.76-2.24-5-5-5-2.28 0-4.27 1.54-4.84 3.75-.14.54.18 1.08.72 1.22.53.14 1.08-.18 1.22-.72C9.44 3.93 10.63 3 12 3c1.65 0 3 1.35 3 3v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm0 11c0 .55-.45 1-1 1H7c-.55 0-1-.45-1-1v-8c0-.55.45-1 1-1h10c.55 0 1 .45 1 1v8z" />
|
||||
</symbol>
|
||||
<!-- from: action/restore_from_trash/materialiconsround/24px.svg -->
|
||||
<symbol id="icon-restore_from_trash" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2H8c-1.1 0-2 .9-2 2v10zm5.65-8.65c.2-.2.51-.2.71 0L16 14h-2v4h-4v-4H8l3.65-3.65zM15.5 4l-.71-.71c-.18-.18-.44-.29-.7-.29H9.91c-.26 0-.52.11-.7.29L8.5 4H6c-.55 0-1 .45-1 1s.45 1 1 1h12c.55 0 1-.45 1-1s-.45-1-1-1h-2.5z" />
|
||||
</symbol>
|
||||
<!-- from: communication/import_export/materialiconsround/24px.svg -->
|
||||
<symbol id="icon-import_export" viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 19 KiB |
@@ -19,12 +19,33 @@
|
||||
{% block content %}
|
||||
<h1>{% trans user_name=target_user.localpart %}Edit user {{ user_name }}{% endtrans %}</h1>
|
||||
<form method="POST">{{ form.csrf_token }}<div class="form layout-expanded">
|
||||
{% if target_user.deletion_request %}
|
||||
<div class="box alert">
|
||||
<header>{% trans %}This user account is pending deletion{% endtrans %}</header>
|
||||
<p>{% trans date=target_user.deletion_request.deleted_at | format_datetime %}The owner of the account sent a deletion request on {{ date }} using their app.{% endtrans %}
|
||||
<p>{% trans time=(target_user.deletion_request.pending_until - now())|format_timedelta %}The account has been locked, and will be automatically deleted permanently in {{ time }}.{% endtrans %}</p>
|
||||
|
||||
<p>{% trans %}If this was a mistake, you can cancel the deletion and restore the account.{% endtrans %}</p>
|
||||
|
||||
{%- call form_button("restore_from_trash", form.action_restore, class="secondary") %}{% endcall %}
|
||||
</div>
|
||||
{% elif not target_user.enabled %}
|
||||
<div class="box alert">
|
||||
<header>{% trans %}This user account is locked{% endtrans %}</header>
|
||||
<p>{% trans %}The user will not be able to log in to their account until it is unlocked again.{% endtrans %}</p>
|
||||
|
||||
{%- call form_button("lock_open", form.action_enable, class="secondary") %}{% endcall %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<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 }}
|
||||
@@ -63,14 +84,14 @@
|
||||
{% 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 -%}
|
||||
{%- call form_button("passwd", form.action_create_reset, class="secondary") -%}{%- 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") -%}
|
||||
{%- call standard_button("bug_report", url_for(".debug_user", localpart=target_user.localpart), class="secondary") -%}
|
||||
{%- trans -%}Show debug information{%- endtrans -%}
|
||||
{%- endcall -%}
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{% extends "admin_app.html" %}
|
||||
{% from "library.j2" import action_button, icon, value_or_hint, custom_form_button %}
|
||||
{% from "library.j2" import action_button, avatar, icon, render_user, value_or_hint, custom_form_button with context %}
|
||||
{% block content %}
|
||||
<h1>{% trans %}Manage users{% endtrans %}</h1>
|
||||
<div class="elevated el-2"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans %}Login name{% endtrans %}</th>
|
||||
<th>{% trans %}Display name{% endtrans %}</th>
|
||||
<th>{% trans %}User{% endtrans %}</th>
|
||||
<th>{% trans %}Last active{% endtrans %}</th>
|
||||
<th>{% trans %}Actions{% endtrans %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -14,15 +14,15 @@
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<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 -%}
|
||||
{%- call render_user(user) -%}{%- endcall -%}
|
||||
</td>
|
||||
<td>{% call value_or_hint(user.display_name) %}{% endcall %}</td>
|
||||
{% if user.enabled %}
|
||||
<td>{{ user.last_active | format_last_activity }}</td>
|
||||
{% elif user.deletion_request %}
|
||||
<td>{% trans %}Deleted{% endtrans %}</td>
|
||||
{% else %}
|
||||
<td>{% trans %}Locked{% endtrans %}</td>
|
||||
{% endif %}
|
||||
<td class="nowrap">
|
||||
{%- call action_button("edit", url_for(".edit_user", localpart=user.localpart), class="primary") -%}
|
||||
{% trans user_name=user.localpart %}Edit user {{ user_name }}{% endtrans %}
|
||||
|
||||
@@ -10,6 +10,29 @@
|
||||
{%- endif -%}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro render_user(user, caller=None) -%}
|
||||
<div class="username-with-avatar">
|
||||
<div class="avatar-container">
|
||||
{%- call avatar(user.localpart+"@"+config["SNIKKET_DOMAIN"], user.avatar_info[0].hash if user.avatar_info | length > 0 else None ) %}{% endcall -%}
|
||||
{%- if user.has_admin_role -%}
|
||||
<div class="user-badge-icon">
|
||||
<span class="with-tooltip above" data-tooltip="{% trans %}The user is an administrator.{% endtrans %}">{% call icon("admin") %}{% trans %} (Administrator){% endtrans %}{% endcall %}</span>
|
||||
</div>
|
||||
{%- elif user.has_restricted_role -%}
|
||||
<div class="user-badge-icon">
|
||||
<span class="with-tooltip above" data-tooltip="{% trans %}The user is restricted.{% endtrans %}">{% call icon("lock") %}{% trans %} (Restricted){% endtrans %}{% endcall %}</span>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
<div class="user-info-container">
|
||||
<div class="user-localpart">{{- user.localpart -}}</div>
|
||||
{%- if user.display_name %}
|
||||
<div class="user-display-name">{{- user.display_name -}}</div>
|
||||
{%- endif %}
|
||||
</div>
|
||||
</div>
|
||||
{%- endmacro -%}
|
||||
|
||||
{% macro showuri(uri, caller=None, id_=None) %}
|
||||
{%- if uri is none -%}
|
||||
<em>—</em>
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2023-11-06 13:46+0000\n"
|
||||
"POT-Creation-Date: 2023-12-08 12:08+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -19,17 +19,15 @@ msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:69 snikket_web/templates/admin_delete_user.html:10
|
||||
#: snikket_web/templates/admin_edit_circle.html:73
|
||||
#: snikket_web/templates/admin_users.html:8
|
||||
msgid "Login name"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:73 snikket_web/templates/admin_delete_user.html:12
|
||||
#: snikket_web/templates/admin_edit_circle.html:74
|
||||
#: snikket_web/templates/admin_users.html:9 snikket_web/user.py:63
|
||||
#: snikket_web/templates/admin_edit_circle.html:74 snikket_web/user.py:63
|
||||
msgid "Display name"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:77 snikket_web/templates/admin_edit_user.html:32
|
||||
#: snikket_web/admin.py:77 snikket_web/templates/admin_edit_user.html:53
|
||||
msgid "Access Level"
|
||||
msgstr ""
|
||||
|
||||
@@ -50,187 +48,228 @@ msgid "Update user"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:90
|
||||
msgid "Restore account"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:94
|
||||
msgid "Unlock account"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:98
|
||||
msgid "Create password reset link"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:108
|
||||
#: snikket_web/admin.py:116
|
||||
msgid "Password reset link created"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:123
|
||||
msgid "User information updated."
|
||||
#: snikket_web/admin.py:128
|
||||
msgid "User account restored"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:133
|
||||
msgid "User account unlocked"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:140
|
||||
msgid "Could not restore user account"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:145
|
||||
msgid "Could not unlock user account"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:157
|
||||
msgid "User information updated."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:179
|
||||
msgid "Delete user permanently"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:158
|
||||
#: snikket_web/admin.py:192
|
||||
msgid "User deleted"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:196
|
||||
#: snikket_web/admin.py:230
|
||||
msgid "Password reset link not found"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:208
|
||||
#: snikket_web/admin.py:242
|
||||
msgid "Password reset link deleted"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:228
|
||||
#: snikket_web/admin.py:262
|
||||
msgid "Invite to circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:234
|
||||
#: snikket_web/admin.py:268
|
||||
msgid "At least one circle must be selected"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:239
|
||||
#: snikket_web/admin.py:273
|
||||
msgid "Valid for"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:241
|
||||
#: snikket_web/admin.py:275
|
||||
msgid "One hour"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:242
|
||||
#: snikket_web/admin.py:276
|
||||
msgid "Twelve hours"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:243
|
||||
#: snikket_web/admin.py:277
|
||||
msgid "One day"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:244
|
||||
#: snikket_web/admin.py:278
|
||||
msgid "One week"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:245
|
||||
#: snikket_web/admin.py:279
|
||||
msgid "Four weeks"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:251 snikket_web/templates/admin_edit_invite.html:17
|
||||
#: snikket_web/admin.py:285 snikket_web/templates/admin_edit_invite.html:17
|
||||
msgid "Invitation type"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:253 snikket_web/templates/library.j2:116
|
||||
#: snikket_web/admin.py:287 snikket_web/templates/library.j2:139
|
||||
msgid "Individual"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:254 snikket_web/templates/library.j2:114
|
||||
#: snikket_web/admin.py:288 snikket_web/templates/library.j2:137
|
||||
msgid "Group"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:260
|
||||
#: snikket_web/admin.py:294
|
||||
msgid "New invitation link"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:322
|
||||
#: snikket_web/admin.py:356
|
||||
msgid "Revoke"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:346
|
||||
#: snikket_web/admin.py:380
|
||||
msgid "Invitation created"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:362
|
||||
#: snikket_web/admin.py:396
|
||||
msgid "No such invitation exists"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:377
|
||||
#: snikket_web/admin.py:411
|
||||
msgid "Invitation revoked"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:394 snikket_web/admin.py:442
|
||||
#: snikket_web/admin.py:428 snikket_web/admin.py:476
|
||||
#: snikket_web/templates/admin_delete_circle.html:10
|
||||
#: snikket_web/templates/admin_edit_circle.html:44
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:399 snikket_web/templates/admin_circles.html:47
|
||||
#: snikket_web/admin.py:433 snikket_web/templates/admin_circles.html:47
|
||||
msgid "Create circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:429
|
||||
#: snikket_web/admin.py:463
|
||||
msgid "Circle created"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:447
|
||||
#: snikket_web/admin.py:481
|
||||
msgid "Select user"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:452
|
||||
#: snikket_web/admin.py:486
|
||||
msgid "Update circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:458
|
||||
#: snikket_web/admin.py:492
|
||||
msgid "Add user"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:476 snikket_web/admin.py:575 snikket_web/admin.py:623
|
||||
#: snikket_web/admin.py:510 snikket_web/admin.py:609 snikket_web/admin.py:657
|
||||
msgid "No such circle exists"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:513
|
||||
#: snikket_web/admin.py:547
|
||||
msgid "Circle data updated"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:523
|
||||
#: snikket_web/admin.py:557
|
||||
msgid "User added to circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:532
|
||||
#: snikket_web/admin.py:566
|
||||
msgid "User removed from circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:541
|
||||
#: snikket_web/admin.py:575
|
||||
msgid "Chat removed from circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:559
|
||||
#: snikket_web/admin.py:593
|
||||
msgid "Delete circle permanently"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:586
|
||||
#: snikket_web/admin.py:620
|
||||
msgid "Circle deleted"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:600
|
||||
#: snikket_web/admin.py:634
|
||||
msgid "Group chat name"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:605
|
||||
#: snikket_web/admin.py:639
|
||||
msgid "Create group chat"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:635
|
||||
#: snikket_web/admin.py:669
|
||||
msgid "New group chat added to circle"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:702
|
||||
#: snikket_web/admin.py:736
|
||||
msgid "Message contents"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:708
|
||||
#: snikket_web/admin.py:742
|
||||
msgid "Only send to online users"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:712
|
||||
#: snikket_web/admin.py:746
|
||||
msgid "Post to all users"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:716
|
||||
#: snikket_web/admin.py:750
|
||||
msgid "Send preview to yourself"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/admin.py:738
|
||||
#: snikket_web/admin.py:772
|
||||
msgid "Announcement sent!"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/infra.py:53
|
||||
#: snikket_web/infra.py:56
|
||||
msgid "Main"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/infra.py:78
|
||||
msgid "Never"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/infra.py:95
|
||||
msgid "Today"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/infra.py:101
|
||||
msgid "Yesterday"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/infra.py:105
|
||||
#, python-format
|
||||
msgid "%(time)s ago"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/invite.py:35
|
||||
msgid ""
|
||||
"The account data you tried to import is too large to upload. Please "
|
||||
@@ -639,7 +678,7 @@ msgid "Delete user %(user_name)s"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:6
|
||||
#: snikket_web/templates/admin_edit_user.html:53
|
||||
#: snikket_web/templates/admin_edit_user.html:74
|
||||
msgid "Delete user"
|
||||
msgstr ""
|
||||
|
||||
@@ -708,7 +747,7 @@ msgid "The user has been deleted from the server."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_circle.html:85
|
||||
#: snikket_web/templates/library.j2:108
|
||||
#: snikket_web/templates/library.j2:131
|
||||
msgid "deleted"
|
||||
msgstr ""
|
||||
|
||||
@@ -808,56 +847,90 @@ msgstr ""
|
||||
msgid "Edit user %(user_name)s"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:22
|
||||
msgid "Edit user"
|
||||
#: snikket_web/templates/admin_edit_user.html:24
|
||||
msgid "This user account is pending deletion"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:25
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The owner of the account sent a deletion request on %(date)s using their "
|
||||
"app."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:26
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The account has been locked, and will be automatically deleted "
|
||||
"permanently in %(time)s."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:28
|
||||
msgid ""
|
||||
"If this was a mistake, you can cancel the deletion and restore the "
|
||||
"account."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:34
|
||||
msgid "This user account is locked"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:35
|
||||
msgid ""
|
||||
"The user will not be able to log in to their account until it is unlocked"
|
||||
" again."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:41
|
||||
msgid "Edit user"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:46
|
||||
msgid "The login name cannot be changed."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:33
|
||||
#: snikket_web/templates/admin_edit_user.html:54
|
||||
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:40
|
||||
#: snikket_web/templates/admin_edit_user.html:61
|
||||
#, python-format
|
||||
msgid "<strong>%(title)s%(icon)s</strong><p>%(description)s</p>"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:50
|
||||
#: snikket_web/templates/admin_edit_user.html:71
|
||||
msgid "Return to user list"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:58
|
||||
#: snikket_web/templates/admin_edit_user.html:79
|
||||
msgid "Further actions"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:60
|
||||
#: snikket_web/templates/admin_edit_user.html:81
|
||||
msgid "Reset password"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:63
|
||||
#: snikket_web/templates/admin_edit_user.html:84
|
||||
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:68
|
||||
#: snikket_web/templates/admin_edit_user.html:89
|
||||
msgid "Debug information"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_user.html:70
|
||||
#: snikket_web/templates/admin_edit_user.html:91
|
||||
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:74
|
||||
#: snikket_web/templates/admin_edit_user.html:95
|
||||
msgid "Show debug information"
|
||||
msgstr ""
|
||||
|
||||
@@ -1048,20 +1121,20 @@ msgid ""
|
||||
"your Snikket server. Use it wisely."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_users.html:19
|
||||
msgid "The user is an administrator."
|
||||
#: snikket_web/templates/admin_users.html:8
|
||||
msgid "User"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_users.html:19
|
||||
msgid " (Administrator)"
|
||||
#: snikket_web/templates/admin_users.html:9
|
||||
msgid "Last active"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_users.html:22
|
||||
msgid "The user is restricted."
|
||||
msgid "Deleted"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_users.html:22
|
||||
msgid " (Restricted)"
|
||||
#: snikket_web/templates/admin_users.html:24
|
||||
msgid "Locked"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/app.html:4
|
||||
@@ -1455,19 +1528,35 @@ msgstr ""
|
||||
msgid "First install Snikket from F-Droid using the button below:"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/library.j2:18
|
||||
#: snikket_web/templates/library.j2:19
|
||||
msgid "The user is an administrator."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/library.j2:19
|
||||
msgid " (Administrator)"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/library.j2:23
|
||||
msgid "The user is restricted."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/library.j2:23
|
||||
msgid " (Restricted)"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/library.j2:41
|
||||
msgid "Copy link"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/library.j2:81
|
||||
#: snikket_web/templates/library.j2:104
|
||||
msgid "Invalid input"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/library.j2:122
|
||||
#: snikket_web/templates/library.j2:145
|
||||
msgid "Can be used multiple times to create accounts on this Snikket service."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/library.j2:124
|
||||
#: snikket_web/templates/library.j2:147
|
||||
msgid "Can be used once to create an account on this Snikket service."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ action/logout:logout
|
||||
action/login:login
|
||||
action/exit_to_app:exit_to_app
|
||||
action/lock:lock
|
||||
action/lock_open:lock_open
|
||||
action/restore_from_trash:restore_from_trash
|
||||
communication/import_export:import_export
|
||||
communication/qr_code:qrcode
|
||||
communication/vpn_key:passwd
|
||||
|
||||
6
tools/import-icons.sh
Normal file → Executable file
6
tools/import-icons.sh
Normal file → Executable file
@@ -9,9 +9,9 @@ set -euo pipefail
|
||||
# FLAVOR one of '', 'round', 'sharp', 'outlined', 'twoshade'
|
||||
# SVGOUT path to the newly created SVG file
|
||||
root="$1/src"
|
||||
iconlist_file="$2"
|
||||
flavor="$3"
|
||||
output_file="$4"
|
||||
iconlist_file="${2-tools/icons.list}"
|
||||
flavor="${3-round}"
|
||||
output_file="${4-snikket_web/static/img/icons.svg}"
|
||||
|
||||
printf '<svg aria-hidden="true" style="position: absolute; width: 0; height: 0; overflow: hidden;" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">\n<defs>\n' > "$output_file"
|
||||
printf '<!-- These icons are sourced from Google’s Material Icons set,\nlicensed under the terms of the Apache 2.0 License -->\n' >> "$output_file"
|
||||
|
||||
Reference in New Issue
Block a user