forked from Github-Mirrors/canaille
Every list of items is paginated server-side.
This commit is contained in:
parent
480b085db3
commit
e5d968d4f5
23 changed files with 631 additions and 134 deletions
|
@ -38,6 +38,7 @@ from .forms import LoginForm
|
|||
from .forms import PasswordForm
|
||||
from .forms import PasswordResetForm
|
||||
from .forms import profile_form
|
||||
from .forms import TableForm
|
||||
from .mails import send_invitation_mail
|
||||
from .mails import send_password_initialization_mail
|
||||
from .mails import send_password_reset_mail
|
||||
|
@ -179,11 +180,18 @@ def firstlogin(uid):
|
|||
return render_template("firstlogin.html", form=form, uid=uid)
|
||||
|
||||
|
||||
@bp.route("/users")
|
||||
@bp.route("/users", methods=["GET", "POST"])
|
||||
@permissions_needed("manage_users")
|
||||
def users(user):
|
||||
users = User.query()
|
||||
return render_template("users.html", users=users, menuitem="users")
|
||||
table_form = TableForm(User, formdata=request.form)
|
||||
if request.form and not table_form.validate():
|
||||
abort(404)
|
||||
|
||||
return render_template(
|
||||
"users.html",
|
||||
menuitem="users",
|
||||
table_form=table_form,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import math
|
||||
|
||||
import wtforms.form
|
||||
from flask import current_app
|
||||
from flask import g
|
||||
|
@ -41,6 +43,25 @@ def existing_login(form, field):
|
|||
)
|
||||
|
||||
|
||||
class TableForm(FlaskForm):
|
||||
def __init__(self, cls=None, page_size=25, filter=None, **kwargs):
|
||||
filter = filter or {}
|
||||
super().__init__(**kwargs)
|
||||
self.items = cls.query(**filter)
|
||||
self.page_size = page_size
|
||||
self.nb_items = len(self.items)
|
||||
self.page_max = max(1, math.ceil(self.nb_items / self.page_size))
|
||||
first_item = (self.page.data - 1) * self.page_size
|
||||
last_item = min((self.page.data) * self.page_size, self.nb_items)
|
||||
self.items_slice = self.items[first_item:last_item]
|
||||
|
||||
page = wtforms.IntegerField(default=1)
|
||||
|
||||
def validate_page(self, field):
|
||||
if field.data < 1 or field.data > self.page_max:
|
||||
raise wtforms.validators.ValidationError(_("The page number is not valid"))
|
||||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
login = wtforms.StringField(
|
||||
_("Login"),
|
||||
|
|
|
@ -10,16 +10,21 @@ from flask_themer import render_template
|
|||
from .flaskutils import permissions_needed
|
||||
from .forms import CreateGroupForm
|
||||
from .forms import EditGroupForm
|
||||
from .forms import TableForm
|
||||
from .models import Group
|
||||
from .models import User
|
||||
|
||||
bp = Blueprint("groups", __name__, url_prefix="/groups")
|
||||
|
||||
|
||||
@bp.route("/")
|
||||
@bp.route("/", methods=["GET", "POST"])
|
||||
@permissions_needed("manage_groups")
|
||||
def groups(user):
|
||||
groups = Group.query()
|
||||
return render_template("groups.html", groups=groups, menuitem="groups")
|
||||
table_form = TableForm(Group, formdata=request.form)
|
||||
if request.form and request.form.get("page") and not table_form.validate():
|
||||
abort(404)
|
||||
|
||||
return render_template("groups.html", menuitem="groups", table_form=table_form)
|
||||
|
||||
|
||||
@bp.route("/add", methods=("GET", "POST"))
|
||||
|
@ -53,7 +58,11 @@ def group(user, groupname):
|
|||
if not group:
|
||||
abort(404)
|
||||
|
||||
if request.method == "GET" or request.form.get("action") == "edit":
|
||||
if (
|
||||
request.method == "GET"
|
||||
or request.form.get("action") == "edit"
|
||||
or request.form.get("page")
|
||||
):
|
||||
return edit_group(group)
|
||||
|
||||
if request.form.get("action") == "delete":
|
||||
|
@ -63,6 +72,10 @@ def group(user, groupname):
|
|||
|
||||
|
||||
def edit_group(group):
|
||||
table_form = TableForm(User, filter={"memberOf": group}, formdata=request.form)
|
||||
if request.form and request.form.get("page") and not table_form.validate():
|
||||
abort(404)
|
||||
|
||||
form = EditGroupForm(
|
||||
request.form or None,
|
||||
data={
|
||||
|
@ -71,7 +84,7 @@ def edit_group(group):
|
|||
},
|
||||
)
|
||||
|
||||
if request.form:
|
||||
if request.form and not request.form.get("page"):
|
||||
if form.validate():
|
||||
group.description = [form.description.data]
|
||||
group.save()
|
||||
|
@ -84,7 +97,10 @@ def edit_group(group):
|
|||
flash(_("Group edition failed."), "error")
|
||||
|
||||
return render_template(
|
||||
"group.html", form=form, edited_group=group, members=group.get_members()
|
||||
"group.html",
|
||||
form=form,
|
||||
edited_group=group,
|
||||
table_form=table_form,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1,20 +1,26 @@
|
|||
from canaille.flaskutils import permissions_needed
|
||||
from canaille.forms import TableForm
|
||||
from canaille.oidc.models import AuthorizationCode
|
||||
from flask import abort
|
||||
from flask import Blueprint
|
||||
from flask import request
|
||||
from flask_themer import render_template
|
||||
|
||||
|
||||
bp = Blueprint("authorizations", __name__, url_prefix="/admin/authorization")
|
||||
|
||||
|
||||
@bp.route("/")
|
||||
@bp.route("/", methods=["GET", "POST"])
|
||||
@permissions_needed("manage_oidc")
|
||||
def index(user):
|
||||
authorizations = AuthorizationCode.query()
|
||||
table_form = TableForm(AuthorizationCode, formdata=request.form)
|
||||
if request.form and request.form.get("page") and not table_form.validate():
|
||||
abort(404)
|
||||
|
||||
return render_template(
|
||||
"oidc/admin/authorization_list.html",
|
||||
authorizations=authorizations,
|
||||
menuitem="admin",
|
||||
table_form=table_form,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import datetime
|
||||
|
||||
from canaille.flaskutils import permissions_needed
|
||||
from canaille.forms import TableForm
|
||||
from canaille.oidc.forms import ClientAdd
|
||||
from canaille.oidc.models import Client
|
||||
from flask import abort
|
||||
|
@ -17,12 +18,15 @@ from werkzeug.security import gen_salt
|
|||
bp = Blueprint("clients", __name__, url_prefix="/admin/client")
|
||||
|
||||
|
||||
@bp.route("/")
|
||||
@bp.route("/", methods=["GET", "POST"])
|
||||
@permissions_needed("manage_oidc")
|
||||
def index(user):
|
||||
clients = Client.query()
|
||||
table_form = TableForm(Client, formdata=request.form)
|
||||
if request.form and request.form.get("page") and not table_form.validate():
|
||||
abort(404)
|
||||
|
||||
return render_template(
|
||||
"oidc/admin/client_list.html", clients=clients, menuitem="admin"
|
||||
"oidc/admin/client_list.html", menuitem="admin", table_form=table_form
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import datetime
|
||||
|
||||
from canaille.flaskutils import permissions_needed
|
||||
from canaille.forms import TableForm
|
||||
from canaille.models import User
|
||||
from canaille.oidc.models import Client
|
||||
from canaille.oidc.models import Token
|
||||
|
@ -8,20 +9,23 @@ from flask import abort
|
|||
from flask import Blueprint
|
||||
from flask import flash
|
||||
from flask import redirect
|
||||
from flask import request
|
||||
from flask import url_for
|
||||
from flask_babel import gettext as _
|
||||
from flask_themer import render_template
|
||||
|
||||
|
||||
bp = Blueprint("tokens", __name__, url_prefix="/admin/token")
|
||||
|
||||
|
||||
@bp.route("/")
|
||||
@bp.route("/", methods=["GET", "POST"])
|
||||
@permissions_needed("manage_oidc")
|
||||
def index(user):
|
||||
tokens = Token.query()
|
||||
table_form = TableForm(Token, formdata=request.form)
|
||||
if request.form and request.form.get("page") and not table_form.validate():
|
||||
abort(404)
|
||||
|
||||
return render_template(
|
||||
"oidc/admin/token_list.html", tokens=tokens, menuitem="admin"
|
||||
"oidc/admin/token_list.html", menuitem="admin", table_form=table_form
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -119,7 +119,7 @@ i.massive.massive.massive.portrait.icon, i.massive.massive.massive.portrait.icon
|
|||
.ui.link.menu .item:hover, .ui.menu .dropdown.item:hover, .ui.menu .link.item:hover, .ui.menu a.item:hover, .ui.menu a.item.active:hover {
|
||||
color: rgba(255,255,255,.95);
|
||||
}
|
||||
.ui.attached.header, .ui.toggle.checkbox label::before, .ui.table > thead > tr > th, table tbody tr td .ui.label, .ui.card, .ui.cards > .card, .ui.message, .ui.info.message, .ui.success.message {
|
||||
.ui.attached.header, .ui.toggle.checkbox label::before, .ui.table > thead > tr > th, .ui.table > tfoot > tr > th, table tbody tr td .ui.label, .ui.card, .ui.cards > .card, .ui.message, .ui.info.message, .ui.success.message {
|
||||
background-color: #282828;
|
||||
color: rgba(255,255,255,.87) !important;
|
||||
}
|
||||
|
|
|
@ -87,9 +87,7 @@
|
|||
</div>
|
||||
</h2>
|
||||
|
||||
{% with users=members %}
|
||||
{% include "partial/users.html" %}
|
||||
{% endwith %}
|
||||
{% include "partial/users.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -11,29 +11,6 @@
|
|||
{% trans %}Groups{% endtrans %}
|
||||
</div>
|
||||
</h2>
|
||||
<table class="ui table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{% trans %}Name{% endtrans %}</th>
|
||||
<th>{% trans %}Description{% endtrans %}</th>
|
||||
<th>{% trans %}Number of members{% endtrans %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for group in groups %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('groups.group', groupname=group.name) }}">
|
||||
<i class="users circular black inverted icon"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td><a href="{{ url_for('groups.group', groupname=group.name) }}">{{ group.name }}</a></td>
|
||||
<td>{% if group.description %}{{ group.description[0] }}{% endif %}</td>
|
||||
<td>{{ group.member|len }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% include "partial/groups.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
56
canaille/templates/macro/table.html
Normal file
56
canaille/templates/macro/table.html
Normal file
|
@ -0,0 +1,56 @@
|
|||
{% macro pagination(form) %}
|
||||
<form id="pagination" action="{{ url_for(request.url_rule.endpoint, **request.view_args) }}" method="POST" class="ui form">
|
||||
{{ form.hidden_tag() if form.hidden_tag }}
|
||||
<div class="ui right floated stackable buttons">
|
||||
<span class="icon disabled ui button">
|
||||
{% trans %}Page{% endtrans %}
|
||||
</span>
|
||||
{% if form.page.data > 1 %}
|
||||
<button name="page" type="submit" class="icon ui button" value="{{ form.page.data - 1 }}">
|
||||
<i class="left chevron icon"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="icon disabled ui button">
|
||||
<i class="left chevron icon"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if form.page.data > 1 %}
|
||||
<button name="page" type="submit" class="ui button" value="1">
|
||||
1
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if form.page.data > 2 %}
|
||||
<span class="disabled ui button">
|
||||
…
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="ui button active">
|
||||
{{ form.page.data }}
|
||||
</span>
|
||||
{% if form.page.data < form.page_max - 1 %}
|
||||
<span class="disabled ui button">
|
||||
…
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if form.page.data < form.page_max %}
|
||||
<button name="page" type="submit" class="ui button" value="{{ form.page_max }}">
|
||||
{{ form.page_max }}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if form.page.data < form.page_max %}
|
||||
<button name="page" type="submit" class="icon ui button" value="{{ form.page.data + 1 }}">
|
||||
<i class="right chevron icon"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="icon disabled ui button">
|
||||
<i class="right chevron icon"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="ui left floated">
|
||||
<span class="disabled ui button">
|
||||
{{ _("%(nb_items)s items", nb_items=form.nb_items) }}
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
{% endmacro %}
|
|
@ -5,26 +5,5 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<table class="ui table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans %}Code{% endtrans %}</th>
|
||||
<th>{% trans %}Client{% endtrans %}</th>
|
||||
<th>{% trans %}Subject{% endtrans %}</th>
|
||||
<th>{% trans %}Created{% endtrans %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for authorization in authorizations %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('oidc.authorizations.view', authorization_id=authorization.authorization_code_id) }}">{{ authorization.authorization_code_id }}</a></td>
|
||||
<td><a href="{{ url_for('oidc.clients.edit', client_id=authorization.client_id) }}">{{ authorization.client_id }}</a></td>
|
||||
<td>{{ authorization.subject }}</td>
|
||||
<td>{{ authorization.issue_date }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% include "partial/oidc/admin/authorization_list.html" %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -10,33 +10,5 @@
|
|||
<a class="ui primary button" href="{{ url_for('oidc.clients.add') }}">{% trans %}Add client{% endtrans %}</a>
|
||||
</div>
|
||||
|
||||
<table class="ui table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{% trans %}Name{% endtrans %}</th>
|
||||
<th>{% trans %}URL{% endtrans %}</th>
|
||||
<th>{% trans %}Created{% endtrans %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for client in clients %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('oidc.clients.edit', client_id=client.client_id) }}">
|
||||
{% if client.logo_uri %}
|
||||
<img class="ui avatar image" src="{{ client.logo_uri }}" alt="Client logo">
|
||||
{% else %}
|
||||
<i class="plug circular inverted black icon"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</td>
|
||||
<td><a href="{{ url_for('oidc.clients.edit', client_id=client.client_id) }}">{{ client.client_name }}</a></td>
|
||||
<td><a href="{{ client.client_uri }}">{{ client.client_uri }}</a></td>
|
||||
<td>{% if client.client_id_issued_at %}{{ client.client_id_issued_at }}{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% include "partial/oidc/admin/client_list.html" %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -5,38 +5,5 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<table class="ui table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans %}Token{% endtrans %}</th>
|
||||
<th>{% trans %}Client{% endtrans %}</th>
|
||||
<th>{% trans %}Subject{% endtrans %}</th>
|
||||
<th>{% trans %}Created{% endtrans %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for token in tokens %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('oidc.tokens.view', token_id=token.token_id) }}">
|
||||
{{ token.token_id }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('oidc.clients.edit', client_id=token.client.client_id) }}">
|
||||
{{ token.client.client_name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for("account.profile_edition", username=token.subject.uid[0]) }}">
|
||||
{{ token.subject.uid[0] }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ token.issue_date }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% include "partial/oidc/admin/token_list.html" %}
|
||||
{% endblock %}
|
||||
|
|
52
canaille/templates/partial/groups.html
Normal file
52
canaille/templates/partial/groups.html
Normal file
|
@ -0,0 +1,52 @@
|
|||
{% import "macro/table.html" as table %}
|
||||
<table class="ui table groups">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{% trans %}Name{% endtrans %}</th>
|
||||
<th>{% trans %}Description{% endtrans %}</th>
|
||||
<th>{% trans %}Number of members{% endtrans %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for group in table_form.items_slice %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('groups.group', groupname=group.name) }}">
|
||||
<i class="users circular black inverted icon"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td><a href="{{ url_for('groups.group', groupname=group.name) }}">{{ group.name }}</a></td>
|
||||
<td>{% if group.description %}{{ group.description[0] }}{% endif %}</td>
|
||||
<td>{{ group.member|len }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<div class="ui icon message">
|
||||
<i class="exclamation icon"></i>
|
||||
<div class="content">
|
||||
{% if request.headers.get("Hx-Request") %}
|
||||
<div class="header">
|
||||
{% trans %}No item matches your request{% endtrans %}
|
||||
</div>
|
||||
<p>{% trans %}Maybe try with different criterias?{% endtrans %}</p>
|
||||
{% else %}
|
||||
<div class="header">
|
||||
{% trans %}There is nothing here{% endtrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th colspan="4">
|
||||
{{ table.pagination(table_form) }}
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
|
@ -0,0 +1,48 @@
|
|||
{% import "macro/table.html" as table %}
|
||||
<table class="ui table codes">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans %}Code{% endtrans %}</th>
|
||||
<th>{% trans %}Client{% endtrans %}</th>
|
||||
<th>{% trans %}Subject{% endtrans %}</th>
|
||||
<th>{% trans %}Created{% endtrans %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for authorization in table_form.items_slice %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('oidc.authorizations.view', authorization_id=authorization.authorization_code_id) }}">{{ authorization.authorization_code_id }}</a></td>
|
||||
<td><a href="{{ url_for('oidc.clients.edit', client_id=authorization.client_id) }}">{{ authorization.client_id }}</a></td>
|
||||
<td>{{ authorization.subject }}</td>
|
||||
<td>{{ authorization.issue_date }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<div class="ui icon message">
|
||||
<i class="exclamation icon"></i>
|
||||
<div class="content">
|
||||
{% if request.headers.get("Hx-Request") %}
|
||||
<div class="header">
|
||||
{% trans %}No item matches your request{% endtrans %}
|
||||
</div>
|
||||
<p>{% trans %}Maybe try with different criterias?{% endtrans %}</p>
|
||||
{% else %}
|
||||
<div class="header">
|
||||
{% trans %}There is nothing here{% endtrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th colspan="4">
|
||||
{{ table.pagination(table_form) }}
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
56
canaille/templates/partial/oidc/admin/client_list.html
Normal file
56
canaille/templates/partial/oidc/admin/client_list.html
Normal file
|
@ -0,0 +1,56 @@
|
|||
{% import "macro/table.html" as table %}
|
||||
<table class="ui table clients">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{% trans %}Name{% endtrans %}</th>
|
||||
<th>{% trans %}URL{% endtrans %}</th>
|
||||
<th>{% trans %}Created{% endtrans %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for client in table_form.items_slice %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('oidc.clients.edit', client_id=client.client_id) }}">
|
||||
{% if client.logo_uri %}
|
||||
<img class="ui avatar image" src="{{ client.logo_uri }}" alt="Client logo">
|
||||
{% else %}
|
||||
<i class="plug circular inverted black icon"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</td>
|
||||
<td><a href="{{ url_for('oidc.clients.edit', client_id=client.client_id) }}">{{ client.client_name }}</a></td>
|
||||
<td><a href="{{ client.client_uri }}">{{ client.client_uri }}</a></td>
|
||||
<td>{% if client.client_id_issued_at %}{{ client.client_id_issued_at }}{% endif %}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<div class="ui icon message">
|
||||
<i class="exclamation icon"></i>
|
||||
<div class="content">
|
||||
{% if request.headers.get("Hx-Request") %}
|
||||
<div class="header">
|
||||
{% trans %}No item matches your request{% endtrans %}
|
||||
</div>
|
||||
<p>{% trans %}Maybe try with different criterias?{% endtrans %}</p>
|
||||
{% else %}
|
||||
<div class="header">
|
||||
{% trans %}There is nothing here{% endtrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th colspan="4">
|
||||
{{ table.pagination(table_form) }}
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
60
canaille/templates/partial/oidc/admin/token_list.html
Normal file
60
canaille/templates/partial/oidc/admin/token_list.html
Normal file
|
@ -0,0 +1,60 @@
|
|||
{% import "macro/table.html" as table %}
|
||||
<table class="ui table tokens">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans %}Token{% endtrans %}</th>
|
||||
<th>{% trans %}Client{% endtrans %}</th>
|
||||
<th>{% trans %}Subject{% endtrans %}</th>
|
||||
<th>{% trans %}Created{% endtrans %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for token in table_form.items_slice %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('oidc.tokens.view', token_id=token.token_id) }}">
|
||||
{{ token.token_id }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('oidc.clients.edit', client_id=token.client.client_id) }}">
|
||||
{{ token.client.client_name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for("account.profile_edition", username=token.subject.uid[0]) }}">
|
||||
{{ token.subject.uid[0] }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ token.issue_date }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<div class="ui icon message">
|
||||
<i class="exclamation icon"></i>
|
||||
<div class="content">
|
||||
{% if request.headers.get("Hx-Request") %}
|
||||
<div class="header">
|
||||
{% trans %}No item matches your request{% endtrans %}
|
||||
</div>
|
||||
<p>{% trans %}Maybe try with different criterias?{% endtrans %}</p>
|
||||
{% else %}
|
||||
<div class="header">
|
||||
{% trans %}There is nothing here{% endtrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th colspan="4">
|
||||
{{ table.pagination(table_form) }}
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
|
@ -1,4 +1,5 @@
|
|||
<table class="ui table">
|
||||
{% import "macro/table.html" as table %}
|
||||
<table class="ui table users">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if user.can_read("jpegPhoto") %}
|
||||
|
@ -19,7 +20,7 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for watched_user in users %}
|
||||
{% for watched_user in table_form.items_slice %}
|
||||
<tr>
|
||||
{% if user.can_read("jpegPhoto") %}
|
||||
<td>
|
||||
|
@ -73,4 +74,11 @@
|
|||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th colspan="5">
|
||||
{{ table.pagination(table_form) }}
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
|
|
@ -4,6 +4,7 @@ from canaille.oidc.models import AuthorizationCode
|
|||
from canaille.oidc.models import Client
|
||||
from canaille.oidc.models import Consent
|
||||
from canaille.oidc.models import Token
|
||||
from werkzeug.security import gen_salt
|
||||
|
||||
|
||||
def test_no_logged_no_access(testclient):
|
||||
|
@ -23,6 +24,48 @@ def test_client_list(testclient, client, logged_admin):
|
|||
assert client.client_name in res.text
|
||||
|
||||
|
||||
def test_client_list_pagination(testclient, logged_admin, client, other_client):
|
||||
res = testclient.get("/admin/client")
|
||||
assert "2 items" in res
|
||||
clients = []
|
||||
for _ in range(25):
|
||||
client = Client(client_id=gen_salt(48), client_name=gen_salt(48))
|
||||
client.save()
|
||||
clients.append(client)
|
||||
|
||||
res = testclient.get("/admin/client")
|
||||
assert "27 items" in res, res.text
|
||||
client_name = res.pyquery(
|
||||
".clients tbody tr:nth-of-type(1) td:nth-of-type(2) a"
|
||||
).text()
|
||||
assert client_name
|
||||
|
||||
form = res.forms["pagination"]
|
||||
res = form.submit(name="page", value="2")
|
||||
assert (
|
||||
client_name not in res.pyquery(".clients tbody tr td:nth-of-type(2) a").text()
|
||||
)
|
||||
for client in clients:
|
||||
client.delete()
|
||||
|
||||
res = testclient.get("/admin/client")
|
||||
assert "2 items" in res
|
||||
|
||||
|
||||
def test_client_list_bad_pages(testclient, logged_admin):
|
||||
res = testclient.get("/admin/client")
|
||||
form = res.forms["pagination"]
|
||||
testclient.post(
|
||||
"/admin/client", {"csrf_token": form["csrf_token"], "page": "2"}, status=404
|
||||
)
|
||||
|
||||
res = testclient.get("/admin/client")
|
||||
form = res.forms["pagination"]
|
||||
testclient.post(
|
||||
"/admin/client", {"csrf_token": form["csrf_token"], "page": "-1"}, status=404
|
||||
)
|
||||
|
||||
|
||||
def test_client_add(testclient, logged_admin):
|
||||
res = testclient.get("/admin/client/add")
|
||||
data = {
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
from canaille.oidc.models import AuthorizationCode
|
||||
from werkzeug.security import gen_salt
|
||||
|
||||
|
||||
def test_no_logged_no_access(testclient):
|
||||
testclient.get("/admin/authorization", status=403)
|
||||
|
||||
|
@ -11,6 +15,55 @@ def test_authorizaton_list(testclient, authorization, logged_admin):
|
|||
assert authorization.authorization_code_id in res.text
|
||||
|
||||
|
||||
def test_authorization_list_pagination(testclient, logged_admin, client):
|
||||
res = testclient.get("/admin/authorization")
|
||||
assert "0 items" in res
|
||||
authorizations = []
|
||||
for _ in range(26):
|
||||
code = AuthorizationCode(
|
||||
authorization_code_id=gen_salt(48), client=client, subject=client
|
||||
)
|
||||
code.save()
|
||||
authorizations.append(code)
|
||||
|
||||
res = testclient.get("/admin/authorization")
|
||||
assert "26 items" in res, res.text
|
||||
authorization_code_id = res.pyquery(
|
||||
".codes tbody tr:nth-of-type(1) td:nth-of-type(1) a"
|
||||
).text()
|
||||
assert authorization_code_id
|
||||
|
||||
form = res.forms["pagination"]
|
||||
res = form.submit(name="page", value="2")
|
||||
assert (
|
||||
authorization_code_id
|
||||
not in res.pyquery(".codes tbody tr td:nth-of-type(1) a").text()
|
||||
)
|
||||
for authorization in authorizations:
|
||||
authorization.delete()
|
||||
|
||||
res = testclient.get("/admin/authorization")
|
||||
assert "0 items" in res
|
||||
|
||||
|
||||
def test_authorization_list_bad_pages(testclient, logged_admin):
|
||||
res = testclient.get("/admin/authorization")
|
||||
form = res.forms["pagination"]
|
||||
testclient.post(
|
||||
"/admin/authorization",
|
||||
{"csrf_token": form["csrf_token"], "page": "2"},
|
||||
status=404,
|
||||
)
|
||||
|
||||
res = testclient.get("/admin/authorization")
|
||||
form = res.forms["pagination"]
|
||||
testclient.post(
|
||||
"/admin/authorization",
|
||||
{"csrf_token": form["csrf_token"], "page": "-1"},
|
||||
status=404,
|
||||
)
|
||||
|
||||
|
||||
def test_authorizaton_view(testclient, authorization, logged_admin):
|
||||
res = testclient.get("/admin/authorization/" + authorization.authorization_code_id)
|
||||
for attr in authorization.may() + authorization.must():
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
import datetime
|
||||
|
||||
from canaille.oidc.models import Token
|
||||
from werkzeug.security import gen_salt
|
||||
|
||||
|
||||
def test_no_logged_no_access(testclient):
|
||||
testclient.get("/admin/token", status=403)
|
||||
|
||||
|
@ -11,6 +17,57 @@ def test_token_list(testclient, token, logged_admin):
|
|||
assert token.token_id in res.text
|
||||
|
||||
|
||||
def test_token_list_pagination(testclient, logged_admin, client):
|
||||
res = testclient.get("/admin/token")
|
||||
assert "0 items" in res
|
||||
tokens = []
|
||||
for _ in range(26):
|
||||
token = Token(
|
||||
token_id=gen_salt(48),
|
||||
access_token="my-valid-token",
|
||||
client=client,
|
||||
subject=logged_admin,
|
||||
type=None,
|
||||
refresh_token=gen_salt(48),
|
||||
scope="openid profile",
|
||||
issue_date=(datetime.datetime.now().replace(microsecond=0)),
|
||||
lifetime=3600,
|
||||
)
|
||||
token.save()
|
||||
tokens.append(token)
|
||||
|
||||
res = testclient.get("/admin/token")
|
||||
assert "26 items" in res, res.text
|
||||
token_id = res.pyquery(".tokens tbody tr td:nth-of-type(1) a").text()
|
||||
assert token_id
|
||||
|
||||
form = res.forms["pagination"]
|
||||
res = form.submit(name="page", value="2")
|
||||
assert (
|
||||
token_id
|
||||
not in res.pyquery(".tokens tbody tr:nth-of-type(1) td:nth-of-type(1) a").text()
|
||||
)
|
||||
for token in tokens:
|
||||
token.delete()
|
||||
|
||||
res = testclient.get("/admin/token")
|
||||
assert "0 items" in res
|
||||
|
||||
|
||||
def test_token_list_bad_pages(testclient, logged_admin):
|
||||
res = testclient.get("/admin/token")
|
||||
form = res.forms["pagination"]
|
||||
testclient.post(
|
||||
"/admin/token", {"csrf_token": form["csrf_token"], "page": "2"}, status=404
|
||||
)
|
||||
|
||||
res = testclient.get("/admin/token")
|
||||
form = res.forms["pagination"]
|
||||
testclient.post(
|
||||
"/admin/token", {"csrf_token": form["csrf_token"], "page": "-1"}, status=404
|
||||
)
|
||||
|
||||
|
||||
def test_token_view(testclient, token, logged_admin):
|
||||
res = testclient.get("/admin/token/" + token.token_id)
|
||||
assert token.access_token in res.text
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from canaille.models import Group
|
||||
from canaille.models import User
|
||||
from canaille.populate import fake_groups
|
||||
from canaille.populate import fake_users
|
||||
|
||||
|
||||
|
@ -7,6 +8,43 @@ def test_no_group(app, slapd_connection):
|
|||
assert Group.query() == []
|
||||
|
||||
|
||||
def test_group_list_pagination(testclient, logged_admin, foo_group):
|
||||
res = testclient.get("/groups")
|
||||
assert "1 items" in res
|
||||
|
||||
groups = fake_groups(25)
|
||||
|
||||
res = testclient.get("/groups")
|
||||
assert "26 items" in res, res.text
|
||||
group_name = res.pyquery(
|
||||
".groups tbody tr:nth-of-type(1) td:nth-of-type(2) a"
|
||||
).text()
|
||||
assert group_name
|
||||
|
||||
form = res.forms["pagination"]
|
||||
res = form.submit(name="page", value="2")
|
||||
assert group_name not in res.pyquery(".groups tbody tr td:nth-of-type(2) a").text()
|
||||
for group in groups:
|
||||
group.delete()
|
||||
|
||||
res = testclient.get("/groups")
|
||||
assert "1 items" in res
|
||||
|
||||
|
||||
def test_group_list_bad_pages(testclient, logged_admin):
|
||||
res = testclient.get("/groups")
|
||||
form = res.forms["pagination"]
|
||||
testclient.post(
|
||||
"/groups", {"csrf_token": form["csrf_token"], "page": "2"}, status=404
|
||||
)
|
||||
|
||||
res = testclient.get("/groups")
|
||||
form = res.forms["pagination"]
|
||||
testclient.post(
|
||||
"/groups", {"csrf_token": form["csrf_token"], "page": "-1"}, status=404
|
||||
)
|
||||
|
||||
|
||||
def test_set_groups(app, user, foo_group, bar_group):
|
||||
foo_dns = {m.dn for m in foo_group.get_members()}
|
||||
assert user.dn in foo_dns
|
||||
|
@ -151,3 +189,41 @@ def test_edition_failed(testclient, logged_moderator, foo_group):
|
|||
assert "Group edition failed." in res
|
||||
foo_group = Group.get(foo_group.dn)
|
||||
assert foo_group.name == "foo"
|
||||
|
||||
|
||||
def test_user_list_pagination(testclient, logged_admin, foo_group):
|
||||
res = testclient.get("/groups/foo")
|
||||
assert "1 items" in res
|
||||
|
||||
users = fake_users(25)
|
||||
for user in users:
|
||||
foo_group.add_member(user)
|
||||
foo_group.save()
|
||||
|
||||
res = testclient.get("/groups/foo")
|
||||
assert "26 items" in res, res.text
|
||||
user_name = res.pyquery(".users tbody tr:nth-of-type(1) td:nth-of-type(2) a").text()
|
||||
assert user_name
|
||||
|
||||
form = res.forms["pagination"]
|
||||
res = form.submit(name="page", value="2")
|
||||
assert user_name not in res.pyquery(".users tr td:nth-of-type(2) a").text()
|
||||
for user in users:
|
||||
user.delete()
|
||||
|
||||
res = testclient.get("/groups/foo")
|
||||
assert "1 items" in res
|
||||
|
||||
|
||||
def test_user_list_bad_pages(testclient, logged_admin, foo_group):
|
||||
res = testclient.get("/groups/foo")
|
||||
form = res.forms["pagination"]
|
||||
testclient.post(
|
||||
"/groups/foo", {"csrf_token": form["csrf_token"], "page": "2"}, status=404
|
||||
)
|
||||
|
||||
res = testclient.get("/groups/foo")
|
||||
form = res.forms["pagination"]
|
||||
testclient.post(
|
||||
"/groups/foo", {"csrf_token": form["csrf_token"], "page": "-1"}, status=404
|
||||
)
|
||||
|
|
|
@ -1,9 +1,45 @@
|
|||
from unittest import mock
|
||||
|
||||
from canaille.models import User
|
||||
from canaille.populate import fake_users
|
||||
from webtest import Upload
|
||||
|
||||
|
||||
def test_user_list_pagination(testclient, logged_admin):
|
||||
res = testclient.get("/users")
|
||||
assert "1 items" in res
|
||||
|
||||
users = fake_users(25)
|
||||
|
||||
res = testclient.get("/users")
|
||||
assert "26 items" in res, res.text
|
||||
user_name = res.pyquery(".users tbody tr:nth-of-type(1) td:nth-of-type(2) a").text()
|
||||
assert user_name
|
||||
|
||||
form = res.forms["pagination"]
|
||||
res = form.submit(name="page", value="2")
|
||||
assert user_name not in res.pyquery(".users tbody tr td:nth-of-type(2) a").text()
|
||||
for user in users:
|
||||
user.delete()
|
||||
|
||||
res = testclient.get("/users")
|
||||
assert "1 items" in res
|
||||
|
||||
|
||||
def test_user_list_bad_pages(testclient, logged_admin):
|
||||
res = testclient.get("/users")
|
||||
form = res.forms["pagination"]
|
||||
testclient.post(
|
||||
"/users", {"csrf_token": form["csrf_token"], "page": "2"}, status=404
|
||||
)
|
||||
|
||||
res = testclient.get("/users")
|
||||
form = res.forms["pagination"]
|
||||
testclient.post(
|
||||
"/users", {"csrf_token": form["csrf_token"], "page": "-1"}, status=404
|
||||
)
|
||||
|
||||
|
||||
def test_edition_permission(
|
||||
testclient,
|
||||
slapd_server,
|
||||
|
|
Loading…
Reference in a new issue