diff --git a/project/forms/admin.py b/project/forms/admin.py index 1bf9a63..931ec2d 100644 --- a/project/forms/admin.py +++ b/project/forms/admin.py @@ -2,6 +2,7 @@ from flask_wtf import FlaskForm from flask_babelex import lazy_gettext from wtforms import TextAreaField, SubmitField from wtforms.validators import Optional +from project.forms.widgets import MultiCheckboxField class AdminSettingsForm(FlaskForm): @@ -11,3 +12,8 @@ class AdminSettingsForm(FlaskForm): privacy = TextAreaField(lazy_gettext("Privacy"), validators=[Optional()]) submit = SubmitField(lazy_gettext("Save")) + + +class UpdateUserForm(FlaskForm): + roles = MultiCheckboxField(lazy_gettext("Roles")) + submit = SubmitField(lazy_gettext("Update user")) diff --git a/project/services/user.py b/project/services/user.py index f1e1fb5..9467329 100644 --- a/project/services/user.py +++ b/project/services/user.py @@ -6,20 +6,37 @@ def create_user(email, password): return user_datastore.create_user(email=email, password=hash_password(password)) -def add_roles_to_user(email, role_names): +def add_roles_to_user(email, roles): user = find_user_by_email(email) if user is None: raise ValueError("User with given email does not exist.") - for role_name in role_names: - user_datastore.add_role_to_user(user, role_name) + for role in roles: + user_datastore.add_role_to_user(user, role) def add_admin_roles_to_user(email): add_roles_to_user(email, ["admin", "event_verifier"]) +def remove_roles_from_user(email, roles): + user = find_user_by_email(email) + + for role in roles: + user_datastore.remove_role_from_user(user, role) + + +def remove_all_roles_from_user(email): + user = find_user_by_email(email) + remove_roles_from_user(email, user.roles) + + +def set_roles_for_user(email, roles): + remove_all_roles_from_user(email) + add_roles_to_user(email, roles) + + def upsert_user_role(role_name, role_title, permissions): role = user_datastore.find_or_create_role(role_name) role.title = role_title diff --git a/project/templates/admin/admin.html b/project/templates/admin/admin.html index 409f0aa..8edb663 100644 --- a/project/templates/admin/admin.html +++ b/project/templates/admin/admin.html @@ -19,6 +19,10 @@ {{ _('Admin Units') }} + + {{ _('Users') }} + + {% endblock %} \ No newline at end of file diff --git a/project/templates/admin/update_user.html b/project/templates/admin/update_user.html new file mode 100644 index 0000000..e429795 --- /dev/null +++ b/project/templates/admin/update_user.html @@ -0,0 +1,25 @@ +{% extends "layout.html" %} +{% from "_macros.html" import render_field_with_errors, render_field %} +{% block title %} +{{ _('Update user') }} +{% endblock %} +{% block content %} + +

{{ _('Update user') }}

+ +
+ {{ form.hidden_tag() }} + +
+
+ {{ user.email }} +
+
+ {{ render_field_with_errors(form.roles, ri="multicheckbox") }} +
+
+ + {{ render_field(form.submit) }} +
+ +{% endblock %} diff --git a/project/templates/admin/users.html b/project/templates/admin/users.html new file mode 100644 index 0000000..b33d76c --- /dev/null +++ b/project/templates/admin/users.html @@ -0,0 +1,36 @@ +{% extends "layout.html" %} +{% from "_macros.html" import render_pagination %} +{% block title %} +{{ _('Users') }} +{% endblock %} +{% block content %} + + + +
+ + + + + + + + + {% for user in users %} + + + + + {% endfor %} + +
{{ _('Email') }}
{{ user.email }}{{ _('Edit') }}
+
+ +
{{ render_pagination(pagination) }}
+ +{% endblock %} \ No newline at end of file diff --git a/project/views/admin.py b/project/views/admin.py index f5375d1..ff787b0 100644 --- a/project/views/admin.py +++ b/project/views/admin.py @@ -1,15 +1,18 @@ from project import app, db -from project.models import AdminUnit +from project.models import AdminUnit, User, Role from flask import render_template, flash, url_for, redirect from flask_babelex import gettext from flask_security import roles_required -from project.forms.admin import AdminSettingsForm +from project.forms.admin import AdminSettingsForm, UpdateUserForm from project.services.admin import upsert_settings +from project.services.user import set_roles_for_user from project.views.utils import ( flash_errors, handleSqlError, + get_pagination_urls, ) from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.sql import func @app.route("/admin") @@ -44,3 +47,38 @@ def admin_settings(): flash_errors(form) return render_template("admin/settings.html", form=form) + + +@app.route("/admin/users") +@roles_required("admin") +def admin_users(): + users = User.query.order_by(func.lower(User.email)).paginate() + return render_template( + "admin/users.html", users=users.items, pagination=get_pagination_urls(users) + ) + + +@app.route("/admin/user//update", methods=("GET", "POST")) +@roles_required("admin") +def admin_user_update(id): + user = User.query.get_or_404(id) + + form = UpdateUserForm() + form.roles.choices = [ + (c.name, gettext(c.title)) for c in Role.query.order_by(Role.id).all() + ] + + if form.validate_on_submit(): + set_roles_for_user(user.email, form.roles.data) + + try: + db.session.commit() + flash(gettext("User successfully updated"), "success") + return redirect(url_for("admin_users")) + except SQLAlchemyError as e: + db.session.rollback() + flash(handleSqlError(e), "danger") + else: + form.roles.data = [c.name for c in user.roles] + + return render_template("admin/update_user.html", user=user, form=form) diff --git a/tests/views/test_admin.py b/tests/views/test_admin.py index 8d64b3b..952d4ad 100644 --- a/tests/views/test_admin.py +++ b/tests/views/test_admin.py @@ -58,3 +58,52 @@ def test_admin_settings(client, seeder, utils, app, mocker, db_error): assert settings.legal_notice == "Mein Impressum" assert settings.contact == "Mein Kontakt" assert settings.privacy == "Mein Datenschutz" + + +def test_admin_users(client, seeder, utils, app): + seeder.create_user(admin=True) + user = utils.login() + seeder.create_admin_unit(user, "Meine Crew") + response = client.get("/admin/users") + assert b"test@test.de" in response.data + + +@pytest.mark.parametrize("db_error", [True, False]) +def test_admin_user_update(client, seeder, utils, app, mocker, db, db_error): + user_id, admin_unit_id = seeder.setup_base(True) + other_user_id = seeder.create_user("other@test.de") + + with app.app_context(): + from project.models import User + from project.services.user import set_roles_for_user + + user = User.query.get_or_404(other_user_id) + set_roles_for_user(user.email, ["event_verifier"]) + db.session.commit() + + url = utils.get_url("admin_user_update", id=other_user_id) + response = utils.get_ok(url) + + if db_error: + utils.mock_db_commit(mocker) + + response = utils.post_form( + url, + response, + { + "roles": "admin", + }, + ) + + if db_error: + utils.assert_response_db_error(response) + return + + utils.assert_response_redirect(response, "admin_users") + + with app.app_context(): + from project.models import User + + user = User.query.get_or_404(other_user_id) + assert len(user.roles) == 1 + assert any(r.name == "admin" for r in user.roles)