diff --git a/docker-compose.yml b/docker-compose.yml index 84eb9cd..aeca697 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,9 @@ services: - ./lumi2/ldap.py:/app/lumi2/ldap.py:ro - ./lumi2/usermodel.py:/app/lumi2/usermodel.py:ro - ./lumi2/usermanager.py:/app/lumi2/usermanager.py:ro - - ./lumi2/static/:/app/lumi2/static/:ro + - ./lumi2/static/css:/app/lumi2/static/css:ro + - ./lumi2/static/images/base:/app/lumi2/static/images/base:ro + - ./lumi2/static/images/default:/app/lumi2/static/images/default:ro - ./lumi2/templates/:/app/lumi2/templates/:ro ports: - "8000:80" diff --git a/lumi2/static/images/apple-touch-icon.png b/lumi2/static/images/base/apple-touch-icon.png similarity index 100% rename from lumi2/static/images/apple-touch-icon.png rename to lumi2/static/images/base/apple-touch-icon.png diff --git a/lumi2/static/images/favicon.ico b/lumi2/static/images/base/favicon.ico similarity index 100% rename from lumi2/static/images/favicon.ico rename to lumi2/static/images/base/favicon.ico diff --git a/lumi2/static/images/favicon.svg b/lumi2/static/images/base/favicon.svg similarity index 100% rename from lumi2/static/images/favicon.svg rename to lumi2/static/images/base/favicon.svg diff --git a/lumi2/static/images/og.png b/lumi2/static/images/base/og.png similarity index 100% rename from lumi2/static/images/og.png rename to lumi2/static/images/base/og.png diff --git a/lumi2/static/images/og.svg b/lumi2/static/images/base/og.svg similarity index 100% rename from lumi2/static/images/og.svg rename to lumi2/static/images/base/og.svg diff --git a/lumi2/static/assets/default_user_icon.jpg b/lumi2/static/images/default/user.jpg similarity index 100% rename from lumi2/static/assets/default_user_icon.jpg rename to lumi2/static/images/default/user.jpg diff --git a/lumi2/static/assets/default_user_icon.svg b/lumi2/static/images/default/user.svg similarity index 100% rename from lumi2/static/assets/default_user_icon.svg rename to lumi2/static/images/default/user.svg diff --git a/lumi2/templates/base.html b/lumi2/templates/base.html index b7382b2..6f5fcb4 100644 --- a/lumi2/templates/base.html +++ b/lumi2/templates/base.html @@ -13,16 +13,25 @@ - + - - - + + + + {% with messages = get_flashed_messages() %} + {% if messages %} + + {% endif %} + {% endwith %} {% block content %} {% endblock content %} diff --git a/lumi2/templates/usermanager/user_detail.html b/lumi2/templates/usermanager/user_detail.html new file mode 100644 index 0000000..36d059b --- /dev/null +++ b/lumi2/templates/usermanager/user_detail.html @@ -0,0 +1,19 @@ +{% extends 'base.html' %} + +{% block content %} +

User: {{ user.username }}

+profile picture for user {{ user.username }} +
+ + + + + + + + + + + +
+{% endblock content %} diff --git a/lumi2/usermanager.py b/lumi2/usermanager.py index b81101a..98d7d61 100644 --- a/lumi2/usermanager.py +++ b/lumi2/usermanager.py @@ -1,14 +1,73 @@ """Views for lumi2.""" from flask import ( - Blueprint, render_template + Blueprint, render_template, abort, request, flash ) +import lumi2.ldap as ldap +from lumi2.usermodel import User, Group + bp = Blueprint('usermanager', __name__) + @bp.route('/') def index(): """Home page view.""" return render_template('usermanager/index.html') + + +@bp.route("/user/", methods=("GET", "POST")) +def user_detail(username: str): + """Detail view for a specific User.""" + + try: + conn = ldap.get_connection() + except Exception: + abort(500) + + try: + user = ldap.get_user(conn, username) + except ldap.EntryNotFoundException: + conn.unbind() + abort(404) + + user._generate_static_images() + + if request.method == 'POST': + form_is_valid = True + + if request.form['email']: + user.email = request.form['email'] + if not User.is_valid_email(user.email): + flash("Invalid email address.") + form_is_valid = False + + if request.form['first_name']: + user.first_name = request.form['first_name'] + if not User.is_valid_person_name(user.first_name): + flash("Invalid first name.") + form_is_valid = False + + if request.form['last_name']: + user.last_name = request.form['last_name'] + if not User.is_valid_person_name(user.last_name): + flash("Invalid last name.") + form_is_valid = False + + if request.form['display_name']: + user.display_name = request.form['display_name'] + if not User.is_valid_person_name(user.display_name): + flash("Invalid nickname.") + form_is_valid = False + + if request.form['password']: + user.password_hash = User.generate_password_hash(request.form['password']) + + if form_is_valid: + ldap.update_user(conn, user) + flash("User information was updated!") + + conn.unbind() + return render_template('usermanager/user_detail.html', user=user) diff --git a/lumi2/usermodel.py b/lumi2/usermodel.py index 9e50dc1..07e9a2b 100644 --- a/lumi2/usermodel.py +++ b/lumi2/usermodel.py @@ -4,6 +4,7 @@ from string import ascii_lowercase, ascii_uppercase, digits, whitespace from base64 import b64encode, b64decode import hashlib from binascii import Error as Base64DecodeError +from pathlib import Path from PIL import Image from flask import current_app @@ -260,7 +261,7 @@ class User: The default user profile picture. """ - image_path = f"{current_app.static_folder}/assets/default_user_icon.jpg" + image_path = f"{current_app.static_folder}/images/default/user.jpg" return Image.open(image_path) @@ -304,6 +305,39 @@ class User: self.picture = User._get_default_picture() + def _generate_static_images(self, force=False) -> None: + """Generates the static images for this User's picture on disc. + + The user's full profile picture and a thumbnail are written to + 'static/images/user//full.jpg' + and 'static/images/user//thumbnail.jpg' respectively. + The thumbnail's fixed size is 512x512 px. + + If the parameter force is set to True, existing images are overwritten. + Otherwise, if the images already exist on disk, image generation is skipped. + + Parameters + ---------- + force : bool = False + Whether or not existing images on disk should be regenerated. + """ + + path_to_image_folder = Path(current_app.static_folder) / "images" / "users" / self.username + path_to_full_image = path_to_image_folder / "full.jpg" + path_to_thumbnail = path_to_image_folder / "thumbnail.jpg" + + if not path_to_image_folder.is_dir(): + path_to_image_folder.mkdir(parents=True) + + if not path_to_full_image.is_file() or force: + self.picture.save(path_to_full_image) + + if not path_to_thumbnail.is_file() or force: + thumb = self.picture.copy() + thumb.thumbnail((256, 256)) + thumb.save(path_to_thumbnail) + + def get_dn(self) -> str: """Returns the LDAP DN for the DIT entry representing this User.