You've already forked snikket-web-portal
Compare commits
22 Commits
feature/mu
...
fix/rc2-ru
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7411f4a9e1 | ||
|
|
d63ae4768a | ||
|
|
92a8da724f | ||
|
|
ea3a081b6c | ||
|
|
0647ba2601 | ||
|
|
2769036f94 | ||
|
|
c76befad1c | ||
|
|
74ecfb8653 | ||
|
|
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
|
||||
|
||||
@@ -50,7 +53,7 @@ def flatten(a: typing.Iterable, levels: int = 1) -> typing.Iterable:
|
||||
|
||||
def circle_name(c: typing.Any) -> str:
|
||||
if c.id_ == "default" and c.name == "default":
|
||||
return _("Main")
|
||||
return _l("Main")
|
||||
return c.name
|
||||
|
||||
|
||||
@@ -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", [])
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -131,7 +190,7 @@ class AdminGroupChatInfo:
|
||||
return cls(
|
||||
id_=data["id"],
|
||||
jid=data["jid"],
|
||||
name=data["name"],
|
||||
name=data.get("name", ""),
|
||||
)
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
{% block head_lead %}
|
||||
{{ super() }}
|
||||
<title>{% trans %}Reset your password | Snikket{% endtrans %}</title>
|
||||
<script async type="text/javascript" src="{{ url_for("static", filename="js/qrcode.min.js") }}"></script>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<form method="POST"><div class="form layout-expanded">
|
||||
@@ -27,9 +26,4 @@
|
||||
{%- call form_button("passwd", form.action_reset, class="primary") -%}{%- endcall -%}
|
||||
</div>
|
||||
</div></form>
|
||||
<script type="text/javascript">
|
||||
var onload = function() {
|
||||
apply_qr_code(document.getElementById("qr-uri"));
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -134,7 +134,6 @@
|
||||
|
||||
var onload = function() {
|
||||
apply_qr_code(document.getElementById("qr-invite-page"));
|
||||
apply_qr_code(document.getElementById("qr-uri"));
|
||||
var popover_as = document.getElementsByClassName("popover");
|
||||
for (var i = 0; i < popover_as.length; ++i) {
|
||||
var a = popover_as[i];
|
||||
|
||||
@@ -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>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,28 +8,26 @@ 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-12 18:22+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"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.13.1\n"
|
||||
"Generated-By: Babel 2.14.0\n"
|
||||
|
||||
#: 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
|
||||
@@ -1187,11 +1260,11 @@ msgstr ""
|
||||
msgid "Reset your password | Snikket"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_reset.html:15
|
||||
#: snikket_web/templates/invite_reset.html:14
|
||||
msgid "Reset your password online"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/invite_reset.html:16
|
||||
#: snikket_web/templates/invite_reset.html:15
|
||||
msgid ""
|
||||
"To reset your password online, fill out the fields below and confirm "
|
||||
"using the button."
|
||||
@@ -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 ""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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