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 %}
+
+ {% for message in messages %}
+ - {{ message }}
+ {% endfor %}
+
+ {% 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 }}
+
+
+{% 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.