Skip to content

Commit

Permalink
feat: added "Add User" functionality to user management, closes #151.
Browse files Browse the repository at this point in the history
While implementing this, the user edit form was improved: The flags
are presented better and the password can be updated by an Admin.
  • Loading branch information
redimp committed Oct 23, 2024
1 parent 1bc8a77 commit 2701fbe
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 23 deletions.
13 changes: 7 additions & 6 deletions otterwiki/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def __repr__(self):
if self.allow_write: permissions+="W"
if self.allow_upload: permissions+="U"
if self.is_admin: permissions+="A"
return f"<User '{self.name} <{self.email}>' {permissions}>"
return f"<User {self.id} '{self.name} <{self.email}>' {permissions}>"

def __init__(self):
with app.app_context():
Expand All @@ -78,7 +78,7 @@ def get_user(self, uid=None, email=None):
return self.User.query.filter_by(id=uid).first()
if email is not None:
return self.User.query.filter_by(email=email).first()
raise ValueError("Neither uid nor email given")
return self.User()

def update_user(self, user):
# validation check
Expand All @@ -89,6 +89,7 @@ def update_user(self, user):
user.is_admin = True
db.session.add(user)
db.session.commit()
return user

def delete_user(self, user):
db.session.delete(user)
Expand Down Expand Up @@ -676,11 +677,11 @@ def handle_request_confirmation(*args, **kwargs):
def get_all_user(*args, **kwargs):
return auth_manager.get_all_user(*args, **kwargs)

def get_user(uid):
return auth_manager.get_user(uid)
def get_user(*args, **kwargs):
return auth_manager.get_user(*args, **kwargs)

def update_user(user):
return auth_manager.update_user(user)
def update_user(*args, **kwargs):
return auth_manager.update_user(*args, **kwargs)

def delete_user(user):
return auth_manager.delete_user(user)
Expand Down
80 changes: 75 additions & 5 deletions otterwiki/preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import re
import json
from datetime import datetime
from otterwiki.util import is_valid_email
from flask import (
redirect,
Expand All @@ -23,6 +24,7 @@
get_user,
update_user,
delete_user,
generate_password_hash,
)


Expand Down Expand Up @@ -384,21 +386,81 @@ def user_edit_form(uid):
if not has_permission("ADMIN"):
abort(403)
user = get_user(uid)
if user is None:
abort(404)
if uid is not None and (user is None or user.id is None):
return abort(404)
# render form
return render_template(
"user.html",
title="User",
user=user,
)

def handle_user_add(form):
if not has_permission("ADMIN"):
abort(403)
# get empty user object
user = get_user(None)
# update user from form
user.name = form.get("name").strip() # pyright: ignore
user.email = form.get("email").strip() # pyright: ignore

for value, _ in [
("email_confirmed", "email confirmed"),
("is_admin", "admin"),
("is_approved", "approved"),
("allow_read", "read"),
("allow_write", "write"),
("allow_upload", "upload"),
]:
if getattr(user, value) and not form.get(value):
setattr(user, value, False)
elif not getattr(user, value) and form.get(value):
setattr(user, value, True)

error = []
if empty(user.name): # pyright: ignore
error.append("Name must not be empty")
if get_user(email=user.email) is not None: # pyright: ignore
error.append("User with this email exists")
if not is_valid_email(user.email): # pyright: ignore
error.append("Invalid email address")
# handle password
if len(form.get("password1","")) or len(form.get("password2","")):
if form.get("password1","") != form.get("password2",""):
error.append("Passwords do not match")
else:
user.password_hash = generate_password_hash(form.get("password1")) # pyright: ignore

# check user object
if len(error):
for msg in error:
toast(msg, 'danger')
return render_template(
"user.html",
title="User",
user=user,
)
# no error: store in database
user.first_seen=datetime.now() # pyright: ignore
user.last_seen=datetime.now() # pyright: ignore
try:
user = update_user(user)
# send_approvement_mail(user)
app.logger.info(f"{user.name} <{user.email}> added")
toast(f"{user.name} <{user.email}> added")
except Exception as e:
app.logger.error(f"Unable to update user: {e}")
toast('Unable to create user. Please check the server logs.', 'danger')
return redirect(url_for("user", uid=user.id))


def handle_user_edit(uid, form):
if not has_permission("ADMIN"):
abort(403)
if uid is None:
return handle_user_add(form)
user = get_user(uid)
if user is None:
if not user or user.id is None:
abort(404)
msgs, flags = [], []
# delete
Expand All @@ -407,9 +469,9 @@ def handle_user_edit(uid, form):
toast(f"Unable to delete yourself.", "error")
return redirect(url_for("user", uid=user.id))
toast(f"User '{user.name} &lt;{user.email}&gt;' deleted.")
app.logger.info(f"deleted user '{user.name} &lt;{user.email}&gt;'")
app.logger.info(f"deleted user '{user.name} <{user.email}>'")
delete_user(user)
return redirect(url_for("admin", _anchor="user_management"))
return redirect(url_for("admin_user_management"))
if form.get("name") is None:
return redirect(url_for("user", uid=user.id))
# name
Expand All @@ -430,9 +492,16 @@ def handle_user_edit(uid, form):
f"'{form.get('email').strip()}' is not a valid email address",
"danger",
)
if len(form.get("password1","")) or len(form.get("password2","")):
if form.get("password1","") != form.get("password2",""):
toast("Passwords do not match", "danger")
else:
user.password_hash = generate_password_hash(form.get("password1"))
msgs.append("Updated password")
user_was_already_approved = user.is_approved
# handle all the flags
for value, label in [
("email_confirmed", "email confirmed"),
("is_admin", "admin"),
("is_approved", "approved"),
("allow_read", "read"),
Expand Down Expand Up @@ -463,4 +532,5 @@ def handle_user_edit(uid, form):
send_approvement_mail(user)
except Exception as e:
app.logger.error(f"Unable to update user: {e}")
toast('Unable to update the user. Please check the server logs.', 'danger')
return redirect(url_for("user", uid=user.id))
1 change: 1 addition & 0 deletions otterwiki/templates/admin/user_management.html
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ <h2 class="card-title">User Management</h2>
<div class="mt-10">
<input class="btn btn-primary" name="update_permissions" type="submit" value="Update Privileges" {% if not
auth_supported_features['editing'] %}disabled{% endif %}>
{% if auth_supported_features['editing'] %}&nbsp;<a href="{{ url_for("user", uid="") }}" class="btn">Add User</a>{% endif %}
</div>
</form>
</div>
Expand Down
58 changes: 47 additions & 11 deletions otterwiki/templates/user.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,80 @@
{% block content %}
<div class="col-sm-12 col-md-12 col-lg-10">
<div class="card mx-auto mx-lg-20">
<h2 class="card-title">Edit User</h2>
<h2 class="card-title">
{% if user.id %}
Edit User
{% else %}
Add User
{% endif %}
</h2>
<form action="{{ url_for("user", uid=user.id) }}" method="POST" class="form-inline">
<div class="form-group">
<label for="name" class="required w-md-100">Name</label>
<label for="name" class="required w-md-200">Name</label>
<input type="text" class="form-control" name="name" id="name" placeholder="" value="{{user.name if user.name}}">
</div>
<div class="form-group">
<label for="name" class="required w-md-100">eMail</label>
<label for="name" class="required w-md-200">eMail</label>
<input type="text" class="form-control" name="email" id="email" placeholder="" value="{{user.email if user.email}}">
</div>
<div class="form-group">
<label for="password1" class="w-md-200">Password</label>
<input type="password" class="form-control" name="password1" id="password1" placeholder="" value="">
</div>
<div class="form-group">
<label for="password2" class="w-md-200">Confirm password</label>
<input type="password" class="form-control" name="password2" id="password2" placeholder="" value="">
</div>
<h3 class="card-title">Flags and Permissions</h3>
<div class="container-fluid">
<div class="row">
{%
for label, name, value, note in [
("Approved","is_approved",user.is_approved,""),
("Read","allow_read",user.allow_read,""),
("Write","allow_write",user.allow_write,""),
("Upload","allow_upload",user.allow_upload,""),
("Admin","is_admin",user.is_admin,"Users flagged as admins always have <strong>all</strong> the permissions."),
for label, name, value, note, new_row in [
(
"eMail confirmed","email_confirmed",
user.email_confirmed,
"If not set, users have to confirm their email address to login." if config.EMAIL_NEEDS_CONFIRMATION else "User don't need to confirm their eMail address.",
False
),
(
"Approved","is_approved",user.is_approved,
"Auto approval is enabled." if config.AUTO_APPROVAL else "This flag has to be set by an admin.",
True
),
("Read","allow_read",user.allow_read,"", False),
("Write","allow_write",user.allow_write,"", False),
("Upload","allow_upload",user.allow_upload,"", True),
("Admin","is_admin",user.is_admin,"Users flagged as admins always have <strong>all</strong> the permissions.", False),
]
%}
<div class="col-sm-4 pr-5">
<div class="form-group">
<div class="custom-checkbox">
<input type="checkbox" id="{{name}}" name="{{name}}" value="1" {{"checked=\"checked\"" if value }}>
<label for="{{name}}" class="">{{ label }}</label>
{% if note %}
<div class="pt-5 ml-10" style="display:inline;">
<div class="pt-5" style="display:block;">
<i>{{ note|safe }}</i>
</div>
{% endif %}
</div>
</div>
</div>
{%if new_row %}
</div>
<div class="row">
{% endif %}
{% endfor %}
</div> {# row #}
</div> {# container-fluid #}
{# #}
<input class="btn btn-primary" type="submit" value="Update">
<input class="btn btn-primary" type="submit" value="{% if user.id %}Update{% else %}Add{% endif %}">
<a href="{{ url_for("admin_user_management") }}" class="btn" role="button">Cancel</a>
</form>
</div>{# card #}
</div>{# w-600 container #}

{% if user.id %}
<div class="col-sm-12 col-md-12 col-lg-10">
<div class="card mx-auto m-lg-20">
<h2 class="card-title">Delete User</h2>
Expand All @@ -56,4 +91,5 @@ <h2 class="card-title">Delete User</h2>
</form>
</div>{# card #}
</div>{# w-600 container #}
{% endif %}
{% endblock %}
1 change: 1 addition & 0 deletions otterwiki/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ def admin():
else:
return otterwiki.preferences.handle_preferences(request.form)

@app.route("/-/user/", methods=["POST","GET"])
@app.route("/-/user/<string:uid>", methods=["POST","GET"])
@login_required
def user(uid=None):
Expand Down
47 changes: 46 additions & 1 deletion tests/test_preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# vim: set et ts=8 sts=4 sw=4 ai:

from datetime import datetime
from bs4 import BeautifulSoup

def test_admin_form(admin_client):
rv = admin_client.get("/-/admin")
Expand Down Expand Up @@ -412,7 +413,8 @@ def test_user_management(app_with_user, admin_client):

# check user_management if mail address has is listed
rv = admin_client.get("/-/admin/user_management", follow_redirects=True)
assert tmp_user_mail in rv.data.decode()
table = BeautifulSoup(rv.data.decode(), "html.parser").find("table")
assert tmp_user_mail in str(table)

# test prevention of removing all admins
rv = admin_client.post(
Expand Down Expand Up @@ -488,3 +490,46 @@ def test_user_management(app_with_user, admin_client):
assert admin and admin.is_approved == True and admin.is_admin == True
user = SimpleAuth.User.query.filter_by(id=user.id).first()
assert user and getattr(user, flag) == False

def test_user_add(app_with_user, admin_client):
from otterwiki.auth import SimpleAuth

new_user = ("New User", "[email protected]")
# check user_management if mail address has is listed
rv = admin_client.get("/-/admin/user_management", follow_redirects=True)
table = BeautifulSoup(rv.data.decode(), "html.parser").find("table")
# make user doesn't exist
assert new_user[0] not in str(table)
assert new_user[1] not in str(table)
# create user
rv = admin_client.post(f"/-/user/",
data = {
"name": new_user[0],
"email": new_user[1],
"is_approved": "1",
}, follow_redirects=True)
assert rv.status_code == 200
# check that the user has been created
rv = admin_client.get("/-/admin/user_management", follow_redirects=True)
table = BeautifulSoup(rv.data.decode(), "html.parser").find("table")
# make user exists
assert new_user[0] in str(table)
assert new_user[1] in str(table)
user = SimpleAuth.User.query.filter_by(email=new_user[1]).first()
assert user is not None
assert user.name == new_user[0]
assert user.email == new_user[1]
assert user.is_approved == True
assert user.is_admin == False

# try to add user again
rv = admin_client.post(f"/-/user/",
data = {
"name": "",
"email": new_user[1],
"is_approved": "1",
}, follow_redirects=True)
assert rv.status_code == 200
# check for toast
assert "User with this email exists" in rv.data.decode()
assert "Name must not be empty" in rv.data.decode()

0 comments on commit 2701fbe

Please sign in to comment.