feat(usermanager): add user edit view
@ -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"
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 157 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
@ -13,16 +13,25 @@
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="{{ config.SITE_URL }}">
|
||||
<meta property="og:description" content="{{ config.SITE_DESCRIPTION }}">
|
||||
<meta property="og:image" content="{{ url_for('static', filename='images/og.png') }}">
|
||||
<meta property="og:image" content="{{ url_for('static', filename='images/base/og.png') }}">
|
||||
|
||||
<link rel="icon" href="{{ url_for('static', filename='images/favicon.ico') }}">
|
||||
<link rel="icon" href="{{ url_for('static', filename='images/favicon.svg') }}" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='images/apple-touch-icon.png') }}">
|
||||
<link rel="icon" href="{{ url_for('static', filename='images/base/favicon.ico') }}">
|
||||
<link rel="icon" href="{{ url_for('static', filename='images/base/favicon.svg') }}" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='images/base/apple-touch-icon.png') }}">
|
||||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<ul class=flash-message>
|
||||
{% for message in messages %}
|
||||
<li>{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
</body>
|
||||
|
19
lumi2/templates/usermanager/user_detail.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h1>User: {{ user.username }}</h1>
|
||||
<img src="{{ url_for('static', filename='images/users/' + user.username + '/full.jpg') }}" alt="profile picture for user {{ user.username }}">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<label for="email">Email</label>
|
||||
<input name="email" id="email" type="email" placeholder="{{ user.email }}">
|
||||
<label for="first_name">First name</label>
|
||||
<input name="first_name" id="first_name" placeholder="{{ user.first_name }}">
|
||||
<label for="last_name">Last name</label>
|
||||
<input name="last_name" id="last_name" placeholder="{{ user.last_name }}">
|
||||
<label for="display_name">Nickname</label>
|
||||
<input name="display_name" id="display_name" placeholder="{{ user.display_name }}">
|
||||
<label for="password">Password</label>
|
||||
<input name="password" id="password" type="password" placeholder="********">
|
||||
<input type="submit" value="Update">
|
||||
</form>
|
||||
{% endblock content %}
|
@ -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/<string:username>", 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)
|
||||
|
@ -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/<username>/full.jpg'
|
||||
and 'static/images/user/<username>/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.
|
||||
|
||||
|