Implement group support (we call ’em circles)

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
{% extends "app.html" %}
{% from "library.j2" import icon %}
{% from "library.j2" import standard_button, form_button %}
{% block head_lead %}
<title>Snikket Web Portal</title>
{% endblock %}
@@ -9,11 +9,10 @@
<p class="form-desc">{% trans %}Click below to log yourself out of the web portal. This does not affect any other connected devices.{% endtrans %}</p>
{{ form.csrf_token }}
<div class="f-bbox">
{#- -#}
<a href="{{ url_for('user.index') }}" class="button secondary">{% call icon("back") %}{% endcall %}{% trans %}Back{% endtrans %}</a>
{#- -#}
<button type="submit" class="primary">{% call icon("logout") %}{% endcall %}{% trans %}Sign out{% endtrans %}</button>
{#- -#}
{%- call standard_button("back", url_for("user.index"), class="secondary") -%}
{% trans %}Back{% endtrans %}
{%- endcall -%}
{%- call form_button("logout", form.action_signout, class="primary") %}{% endcall -%}
</div>
</form></div>
{% endblock %}