You've already forked snikket-web-portal
Implement group support (we call ’em circles)
This commit is contained in:
51
snikket_web/templates/admin_circles.html
Normal file
51
snikket_web/templates/admin_circles.html
Normal 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 %}
|
||||
5
snikket_web/templates/admin_create_invite.html
Normal file
5
snikket_web/templates/admin_create_invite.html
Normal file
@@ -0,0 +1,5 @@
|
||||
{% extends "admin_app.html" %}
|
||||
{% block content %}
|
||||
<h1>{% trans %}Create invitation{% endtrans %}</h1>
|
||||
{%- include "admin_create_invite_form.html" -%}
|
||||
{% endblock %}
|
||||
31
snikket_web/templates/admin_create_invite_form.html
Normal file
31
snikket_web/templates/admin_create_invite_form.html
Normal 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>
|
||||
48
snikket_web/templates/admin_edit_circle.html
Normal file
48
snikket_web/templates/admin_edit_circle.html
Normal 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 %}
|
||||
@@ -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 -%}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user