Skip to content

Commit

Permalink
feat: Sidebar shortcuts are configurable. Redesigned the admin interf…
Browse files Browse the repository at this point in the history
…ace.

- The interface of the admin preferences has been redesigned in order to
  be more clearly arranged. Separated pages for General Application
  Preferences, Sidebar, Content and Editing, User Management,
  Permissions and Registration and Mail Preferences.
- Which of the shortcuts (Home, Page Index "A - Z", Changelog and
  Create Page) are displayed in the sidebar can be configured now via
  the sidebar preferences. Shortcuts not displayed in the sidebar are
  displayed in the "three dots menu".
- tests: added test_sidebar.py using Beatifulsoup. Will use this more
  often, makes testing much nicer.

This was discussed in #125.
  • Loading branch information
redimp committed Sep 10, 2024
1 parent f382977 commit d2a617e
Show file tree
Hide file tree
Showing 16 changed files with 1,024 additions and 391 deletions.
259 changes: 203 additions & 56 deletions otterwiki/preferences.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env python

import re
from otterwiki.util import is_valid_email
from flask import (
redirect,
Expand All @@ -14,25 +15,43 @@
from otterwiki.helper import toast, send_mail
from otterwiki.util import empty, is_valid_email
from flask_login import current_user
from otterwiki.auth import has_permission, get_all_user, get_user, update_user, delete_user
from otterwiki.auth import (
has_permission,
get_all_user,
get_user,
update_user,
delete_user,
)


def _update_preference(name, value, commit=False):
entry = Preferences.query.filter_by(name=name).first()
if entry is None:
# create entry
entry = Preferences(name=name, value=value) # pyright: ignore
entry = Preferences(name=name, value=value) # pyright: ignore
entry.value = value
db.session.add(entry)
if commit:
db.session.commit()


def handle_mail_preferences(form):
# handle test_mail (sent via the same form)
if not empty(form.get('test_mail_preferences')):
return handle_test_mail_preferences(form)
error = 0
if not is_valid_email(form.get("mail_sender") or ""):
toast("'{}' is not a valid email address.".format(form.get("mail_sender")), "error")
toast(
"'{}' is not a valid email address.".format(
form.get("mail_sender")
),
"error",
)
error += 1
else:
_update_preference("MAIL_DEFAULT_SENDER", form.get("mail_sender").strip())
_update_preference(
"MAIL_DEFAULT_SENDER", form.get("mail_sender").strip()
)
if empty(form.get("mail_server")):
toast("Mail Server must not be empty.", "error")
error += 1
Expand All @@ -51,8 +70,8 @@ def handle_mail_preferences(form):
else:
_update_preference("MAIL_PORT", mail_port)
# MAIL_USERNAME and MAIL_PASSWORD
_update_preference("MAIL_USERNAME", form.get("mail_user", ""))
if len(form.get("mail_password", ""))>0:
_update_preference("MAIL_USERNAME", form.get("mail_username", ""))
if len(form.get("mail_password", "")) > 0:
_update_preference("MAIL_PASSWORD", form.get("mail_password", ""))
# Encryption
if empty(form.get("mail_security")):
Expand All @@ -65,34 +84,81 @@ def handle_mail_preferences(form):
_update_preference("MAIL_USE_TLS", "False")
_update_preference("MAIL_USE_SSL", "True")
if error < 1:
toast("Mail Preferences upated.")
toast("Mail Preferences updated.")

db.session.commit()
update_app_config()
return redirect(url_for("admin", _anchor="mail_preferences"))
return redirect(url_for("admin_mail_preferences"))


def handle_sidebar_preferences(form):
for checkbox in [
"sidebar_shortcut_home",
"sidebar_shortcut_page_index",
"sidebar_shortcut_changelog",
"sidebar_shortcut_create_page",
]:
_update_preference(checkbox.upper(), form.get(checkbox, "False"))

if not re.match(r"^(|\d+)$", form.get("sidebar_menutree_maxdepth", "")):
toast(
"Invalid value: SIDEBAR_MENUTREE_MAXDEPTH must be an integer or empty"
)
return redirect(url_for("admin_sidebar_preferences"))
else:
_update_preference(
"SIDEBAR_MENUTREE_MAXDEPTH",
form.get("sidebar_menutree_maxdepth", ""),
)
_update_preference(
"SIDEBAR_MENUTREE_MODE", form.get("sidebar_menutree_mode", "")
)
# commit changes to the database
db.session.commit()
update_app_config()
toast("Sidebar Preferences updated.")
return redirect(url_for("admin_sidebar_preferences"))


def handle_app_preferences(form):
for name in ["site_name", "site_logo", "site_description", "site_icon",
"sidebar_menutree_maxdepth","sidebar_menutree_mode", "commit_message",
"git_web_server"]:
_update_preference(name.upper(),form.get(name, ""))
for name in ["READ_access", "WRITE_access", "ATTACHMENT_access"]:
_update_preference(name.upper(),form.get(name, "ANONYMOUS"))
for name in [
"site_name",
"site_logo",
"site_description",
"site_icon",
"commit_message",
]:
_update_preference(name.upper(), form.get(name, ""))
for checkbox in [
"disable_registration",
"auto_approval",
"email_needs_confirmation",
"notify_admins_on_register",
"notify_user_on_approval",
"retain_page_name_case",
"hide_logo",
"retain_page_name_case",
"git_web_server",
]:
_update_preference(checkbox.upper(), form.get(checkbox, "False"))
# commit changes to the database
db.session.commit()
update_app_config()
toast("Application Preferences updated.")
return redirect(url_for("admin"))


def handle_content_and_editing(form):
for name in [
"commit_message",
]:
_update_preference(checkbox.upper(),form.get(checkbox, "False"))
_update_preference(name.upper(), form.get(name, ""))
for checkbox in [
"retain_page_name_case",
"git_web_server",
]:
_update_preference(checkbox.upper(), form.get(checkbox, "False"))
# commit changes to the database
db.session.commit()
update_app_config()
toast("Application Preferences upated.")
return redirect(url_for("admin", _anchor="application_preferences"))
toast("Content and Editing Preferences updated.")
return redirect(url_for("admin_content_and_editing"))



def handle_test_mail_preferences(form):
recipient = form.get("mail_recipient")
Expand All @@ -104,34 +170,53 @@ def handle_test_mail_preferences(form):
body = """OtterWiki Test Mail"""
subject = "OtterWiki Test Mail"
try:
send_mail(subject, [recipient], body, _async=False, raise_on_error=True)
send_mail(
subject, [recipient], body, _async=False, raise_on_error=True
)
except Exception as e:
toast("Error: {}".format(e),"error")
toast("Error: {}".format(e), "error")
else:
toast("Testmail sent to {}.".format(recipient))
else:
toast("Invalid email address: {}".format(recipient),"error")
return redirect(url_for("admin", _anchor="mail_preferences"))
toast("Invalid email address: {}".format(recipient), "error")
return redirect(url_for("admin_mail_preferences"))


def handle_preferences(form):
if not empty(form.get('update_preferences')):
return handle_app_preferences(form)
if not empty(form.get('update_mail_preferences')):
return handle_mail_preferences(form)
if not empty(form.get('test_mail_preferences')):
return handle_test_mail_preferences(form)
if not empty(form.get("update_permissions")):
return handle_user_management(form)


def handle_permissions_and_registration(form):
# handle dropdowns
for name in ["READ_access", "WRITE_access", "ATTACHMENT_access"]:
_update_preference(name.upper(), form.get(name, "ANONYMOUS"))
# handle checkboxes
for checkbox in [
"disable_registration",
"auto_approval",
"email_needs_confirmation",
"notify_admins_on_register",
"notify_user_on_approval",
]:
_update_preference(checkbox.upper(), form.get(checkbox, "False"))
# commit changes to the database
db.session.commit()
update_app_config()
toast("Preferences updated.")
return redirect(url_for("admin_permissions_and_registration"))


def send_approvement_mail(user):
text_body = render_template(
"approvement_notification.txt",
sitename=app.config["SITE_NAME"],
name=user.name,
url=url_for("login", _external=True),
)
subject = "Your account has been approved - {} - An Otter Wiki".format(app.config["SITE_NAME"])
"approvement_notification.txt",
sitename=app.config["SITE_NAME"],
name=user.name,
url=url_for("login", _external=True),
)
subject = "Your account has been approved - {} - An Otter Wiki".format(
app.config["SITE_NAME"]
)
send_mail(subject=subject, recipients=[user.email], text_body=text_body)


Expand Down Expand Up @@ -192,7 +277,7 @@ def handle_user_management(form):
msgs.append("enabled admin")
if len(msgs):
toast("{} {} flag".format(user.email, " and ".join(msgs)))
app.logger.info( # pyright: ignore
app.logger.info( # pyright: ignore
"{} updated {} <{}>: {}".format(
current_user, user.name, user.email, " and ".join(msgs)
)
Expand All @@ -203,7 +288,8 @@ def handle_user_management(form):
if user_was_just_approved:
send_approvement_mail(user)

return redirect(url_for("admin", _anchor="user_management"))
return redirect(url_for("admin_user_management"))


def admin_form():
if not has_permission("ADMIN"):
Expand All @@ -217,6 +303,60 @@ def admin_form():
user_list=user_list,
)


def mail_preferences_form():
if not has_permission("ADMIN"):
abort(403)
# query user
return render_template(
"admin/mail_preferences.html",
title="Mail preferences",
)


def content_and_editing_form():
if not has_permission("ADMIN"):
abort(403)
# query user
return render_template(
"admin/content_and_editing.html",
title="Content and Editing preferences",
)


def permissions_and_registration_form():
if not has_permission("ADMIN"):
abort(403)
# render form
return render_template(
"admin/permissions_and_registration.html",
title="Permissions and Registration",
)


def sidebar_preferences_form():
if not has_permission("ADMIN"):
abort(403)
# render form
return render_template(
"admin/sidebar_preferences.html",
title="Sidebar Preferences",
)


def user_management_form():
if not has_permission("ADMIN"):
abort(403)
# query user
user_list = get_all_user()
# render form
return render_template(
"admin/user_management.html",
title="User Management",
user_list=user_list,
)


def user_edit_form(uid):
if not has_permission("ADMIN"):
abort(403)
Expand All @@ -230,6 +370,7 @@ def user_edit_form(uid):
user=user,
)


def handle_user_edit(uid, form):
if not has_permission("ADMIN"):
abort(403)
Expand All @@ -238,9 +379,9 @@ def handle_user_edit(uid, form):
abort(404)
msgs, flags = [], []
# delete
if (form.get("delete", False)):
if (user == current_user):
toast(f"Unable to delete yourself.","error")
if form.get("delete", False):
if user == current_user:
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;'")
Expand All @@ -250,24 +391,31 @@ def handle_user_edit(uid, form):
return redirect(url_for("user", uid=user.id))
# name
if user.name != form.get("name").strip():
msgs.append(f"renamed '{user.name}' to '{form.get('name').strip()}'")
user.name = form.get("name").strip()
new_name = form.get("name").strip()
if len(new_name) > 0:
msgs.append(f"renamed '{user.name}' to '{new_name}'")
user.name = new_name
else:
toast("User name must not be empty.", "danger")
# email
if user.email != form.get("email").strip():
if is_valid_email(form.get("email").strip()):
msgs.append(f"updated {user.email} to {form.get('email').strip()}")
user.email = form.get("email").strip()
else:
toast(f"'{form.get('email').strip()}' is not a valid email address","danger")
toast(
f"'{form.get('email').strip()}' is not a valid email address",
"danger",
)
user_was_already_approved = user.is_approved
# handle all the flags
for value, label in [
("is_admin", "admin"),
("is_approved", "approved"),
("allow_read", "read"),
("allow_write", "write"),
("allow_upload", "upload"),
]:
("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)
flags.append(f"removed {label}")
Expand All @@ -284,13 +432,12 @@ def handle_user_edit(uid, form):
app.logger.info(
"{} updated {} <{}>: {}".format(
current_user, user.name, user.email, " and ".join(msgs)
))
)
)
try:
update_user(user)
if user.is_approved and not user_was_already_approved:
send_approvement_mail(user)
except Exception as e:
app.logger.error(f"Unable to update user: {e}")
return redirect(url_for("user", uid=user.id))


Loading

0 comments on commit d2a617e

Please sign in to comment.