Every list of items is paginated server-side.

This commit is contained in:
Éloi Rivard 2023-02-25 18:11:19 +01:00
parent 480b085db3
commit e5d968d4f5
23 changed files with 631 additions and 134 deletions

View file

@ -38,6 +38,7 @@ from .forms import LoginForm
from .forms import PasswordForm from .forms import PasswordForm
from .forms import PasswordResetForm from .forms import PasswordResetForm
from .forms import profile_form from .forms import profile_form
from .forms import TableForm
from .mails import send_invitation_mail from .mails import send_invitation_mail
from .mails import send_password_initialization_mail from .mails import send_password_initialization_mail
from .mails import send_password_reset_mail from .mails import send_password_reset_mail
@ -179,11 +180,18 @@ def firstlogin(uid):
return render_template("firstlogin.html", form=form, uid=uid) return render_template("firstlogin.html", form=form, uid=uid)
@bp.route("/users") @bp.route("/users", methods=["GET", "POST"])
@permissions_needed("manage_users") @permissions_needed("manage_users")
def users(user): def users(user):
users = User.query() table_form = TableForm(User, formdata=request.form)
return render_template("users.html", users=users, menuitem="users") if request.form and not table_form.validate():
abort(404)
return render_template(
"users.html",
menuitem="users",
table_form=table_form,
)
@dataclass @dataclass

View file

@ -1,3 +1,5 @@
import math
import wtforms.form import wtforms.form
from flask import current_app from flask import current_app
from flask import g 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): class LoginForm(FlaskForm):
login = wtforms.StringField( login = wtforms.StringField(
_("Login"), _("Login"),

View file

@ -10,16 +10,21 @@ from flask_themer import render_template
from .flaskutils import permissions_needed from .flaskutils import permissions_needed
from .forms import CreateGroupForm from .forms import CreateGroupForm
from .forms import EditGroupForm from .forms import EditGroupForm
from .forms import TableForm
from .models import Group from .models import Group
from .models import User
bp = Blueprint("groups", __name__, url_prefix="/groups") bp = Blueprint("groups", __name__, url_prefix="/groups")
@bp.route("/") @bp.route("/", methods=["GET", "POST"])
@permissions_needed("manage_groups") @permissions_needed("manage_groups")
def groups(user): def groups(user):
groups = Group.query() table_form = TableForm(Group, formdata=request.form)
return render_template("groups.html", groups=groups, menuitem="groups") 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")) @bp.route("/add", methods=("GET", "POST"))
@ -53,7 +58,11 @@ def group(user, groupname):
if not group: if not group:
abort(404) 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) return edit_group(group)
if request.form.get("action") == "delete": if request.form.get("action") == "delete":
@ -63,6 +72,10 @@ def group(user, groupname):
def edit_group(group): 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( form = EditGroupForm(
request.form or None, request.form or None,
data={ data={
@ -71,7 +84,7 @@ def edit_group(group):
}, },
) )
if request.form: if request.form and not request.form.get("page"):
if form.validate(): if form.validate():
group.description = [form.description.data] group.description = [form.description.data]
group.save() group.save()
@ -84,7 +97,10 @@ def edit_group(group):
flash(_("Group edition failed."), "error") flash(_("Group edition failed."), "error")
return render_template( 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,
) )

View file

@ -1,20 +1,26 @@
from canaille.flaskutils import permissions_needed from canaille.flaskutils import permissions_needed
from canaille.forms import TableForm
from canaille.oidc.models import AuthorizationCode from canaille.oidc.models import AuthorizationCode
from flask import abort
from flask import Blueprint from flask import Blueprint
from flask import request
from flask_themer import render_template from flask_themer import render_template
bp = Blueprint("authorizations", __name__, url_prefix="/admin/authorization") bp = Blueprint("authorizations", __name__, url_prefix="/admin/authorization")
@bp.route("/") @bp.route("/", methods=["GET", "POST"])
@permissions_needed("manage_oidc") @permissions_needed("manage_oidc")
def index(user): 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( return render_template(
"oidc/admin/authorization_list.html", "oidc/admin/authorization_list.html",
authorizations=authorizations,
menuitem="admin", menuitem="admin",
table_form=table_form,
) )

View file

@ -1,6 +1,7 @@
import datetime import datetime
from canaille.flaskutils import permissions_needed from canaille.flaskutils import permissions_needed
from canaille.forms import TableForm
from canaille.oidc.forms import ClientAdd from canaille.oidc.forms import ClientAdd
from canaille.oidc.models import Client from canaille.oidc.models import Client
from flask import abort from flask import abort
@ -17,12 +18,15 @@ from werkzeug.security import gen_salt
bp = Blueprint("clients", __name__, url_prefix="/admin/client") bp = Blueprint("clients", __name__, url_prefix="/admin/client")
@bp.route("/") @bp.route("/", methods=["GET", "POST"])
@permissions_needed("manage_oidc") @permissions_needed("manage_oidc")
def index(user): 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( return render_template(
"oidc/admin/client_list.html", clients=clients, menuitem="admin" "oidc/admin/client_list.html", menuitem="admin", table_form=table_form
) )

View file

@ -1,6 +1,7 @@
import datetime import datetime
from canaille.flaskutils import permissions_needed from canaille.flaskutils import permissions_needed
from canaille.forms import TableForm
from canaille.models import User from canaille.models import User
from canaille.oidc.models import Client from canaille.oidc.models import Client
from canaille.oidc.models import Token from canaille.oidc.models import Token
@ -8,20 +9,23 @@ from flask import abort
from flask import Blueprint from flask import Blueprint
from flask import flash from flask import flash
from flask import redirect from flask import redirect
from flask import request
from flask import url_for from flask import url_for
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_themer import render_template from flask_themer import render_template
bp = Blueprint("tokens", __name__, url_prefix="/admin/token") bp = Blueprint("tokens", __name__, url_prefix="/admin/token")
@bp.route("/") @bp.route("/", methods=["GET", "POST"])
@permissions_needed("manage_oidc") @permissions_needed("manage_oidc")
def index(user): 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( return render_template(
"oidc/admin/token_list.html", tokens=tokens, menuitem="admin" "oidc/admin/token_list.html", menuitem="admin", table_form=table_form
) )

View file

@ -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 { .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); 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; background-color: #282828;
color: rgba(255,255,255,.87) !important; color: rgba(255,255,255,.87) !important;
} }

View file

@ -87,9 +87,7 @@
</div> </div>
</h2> </h2>
{% with users=members %} {% include "partial/users.html" %}
{% include "partial/users.html" %}
{% endwith %}
</div> </div>
{% endif %} {% endif %}

View file

@ -11,29 +11,6 @@
{% trans %}Groups{% endtrans %} {% trans %}Groups{% endtrans %}
</div> </div>
</h2> </h2>
<table class="ui table"> {% include "partial/groups.html" %}
<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>
</div> </div>
{% endblock %} {% endblock %}

View 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 %}

View file

@ -5,26 +5,5 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% include "partial/oidc/admin/authorization_list.html" %}
<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>
{% endblock %} {% endblock %}

View file

@ -10,33 +10,5 @@
<a class="ui primary button" href="{{ url_for('oidc.clients.add') }}">{% trans %}Add client{% endtrans %}</a> <a class="ui primary button" href="{{ url_for('oidc.clients.add') }}">{% trans %}Add client{% endtrans %}</a>
</div> </div>
<table class="ui table"> {% include "partial/oidc/admin/client_list.html" %}
<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>
{% endblock %} {% endblock %}

View file

@ -5,38 +5,5 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% include "partial/oidc/admin/token_list.html" %}
<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>
{% endblock %} {% endblock %}

View 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>

View file

@ -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>

View 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>

View 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>

View file

@ -1,4 +1,5 @@
<table class="ui table"> {% import "macro/table.html" as table %}
<table class="ui table users">
<thead> <thead>
<tr> <tr>
{% if user.can_read("jpegPhoto") %} {% if user.can_read("jpegPhoto") %}
@ -19,7 +20,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for watched_user in users %} {% for watched_user in table_form.items_slice %}
<tr> <tr>
{% if user.can_read("jpegPhoto") %} {% if user.can_read("jpegPhoto") %}
<td> <td>
@ -73,4 +74,11 @@
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
<tfoot>
<tr>
<th colspan="5">
{{ table.pagination(table_form) }}
</th>
</tr>
</tfoot>
</table> </table>

View file

@ -4,6 +4,7 @@ from canaille.oidc.models import AuthorizationCode
from canaille.oidc.models import Client from canaille.oidc.models import Client
from canaille.oidc.models import Consent from canaille.oidc.models import Consent
from canaille.oidc.models import Token from canaille.oidc.models import Token
from werkzeug.security import gen_salt
def test_no_logged_no_access(testclient): 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 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): def test_client_add(testclient, logged_admin):
res = testclient.get("/admin/client/add") res = testclient.get("/admin/client/add")
data = { data = {

View file

@ -1,3 +1,7 @@
from canaille.oidc.models import AuthorizationCode
from werkzeug.security import gen_salt
def test_no_logged_no_access(testclient): def test_no_logged_no_access(testclient):
testclient.get("/admin/authorization", status=403) 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 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): def test_authorizaton_view(testclient, authorization, logged_admin):
res = testclient.get("/admin/authorization/" + authorization.authorization_code_id) res = testclient.get("/admin/authorization/" + authorization.authorization_code_id)
for attr in authorization.may() + authorization.must(): for attr in authorization.may() + authorization.must():

View file

@ -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): def test_no_logged_no_access(testclient):
testclient.get("/admin/token", status=403) 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 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): def test_token_view(testclient, token, logged_admin):
res = testclient.get("/admin/token/" + token.token_id) res = testclient.get("/admin/token/" + token.token_id)
assert token.access_token in res.text assert token.access_token in res.text

View file

@ -1,5 +1,6 @@
from canaille.models import Group from canaille.models import Group
from canaille.models import User from canaille.models import User
from canaille.populate import fake_groups
from canaille.populate import fake_users from canaille.populate import fake_users
@ -7,6 +8,43 @@ def test_no_group(app, slapd_connection):
assert Group.query() == [] 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): def test_set_groups(app, user, foo_group, bar_group):
foo_dns = {m.dn for m in foo_group.get_members()} foo_dns = {m.dn for m in foo_group.get_members()}
assert user.dn in foo_dns 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 assert "Group edition failed." in res
foo_group = Group.get(foo_group.dn) foo_group = Group.get(foo_group.dn)
assert foo_group.name == "foo" 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
)

View file

@ -1,9 +1,45 @@
from unittest import mock from unittest import mock
from canaille.models import User from canaille.models import User
from canaille.populate import fake_users
from webtest import Upload 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( def test_edition_permission(
testclient, testclient,
slapd_server, slapd_server,