feat(usermanager): add user edit view
@ -13,7 +13,9 @@ services:
|
|||||||
- ./lumi2/ldap.py:/app/lumi2/ldap.py:ro
|
- ./lumi2/ldap.py:/app/lumi2/ldap.py:ro
|
||||||
- ./lumi2/usermodel.py:/app/lumi2/usermodel.py:ro
|
- ./lumi2/usermodel.py:/app/lumi2/usermodel.py:ro
|
||||||
- ./lumi2/usermanager.py:/app/lumi2/usermanager.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
|
- ./lumi2/templates/:/app/lumi2/templates/:ro
|
||||||
ports:
|
ports:
|
||||||
- "8000:80"
|
- "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:type" content="website">
|
||||||
<meta property="og:url" content="{{ config.SITE_URL }}">
|
<meta property="og:url" content="{{ config.SITE_URL }}">
|
||||||
<meta property="og:description" content="{{ config.SITE_DESCRIPTION }}">
|
<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/base/favicon.ico') }}">
|
||||||
<link rel="icon" href="{{ url_for('static', filename='images/favicon.svg') }}" type="image/svg+xml">
|
<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/apple-touch-icon.png') }}">
|
<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') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<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 %}
|
{% block content %}
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
</body>
|
</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."""
|
"""Views for lumi2."""
|
||||||
|
|
||||||
from flask import (
|
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 = Blueprint('usermanager', __name__)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/')
|
@bp.route('/')
|
||||||
def index():
|
def index():
|
||||||
"""Home page view."""
|
"""Home page view."""
|
||||||
|
|
||||||
return render_template('usermanager/index.html')
|
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
|
from base64 import b64encode, b64decode
|
||||||
import hashlib
|
import hashlib
|
||||||
from binascii import Error as Base64DecodeError
|
from binascii import Error as Base64DecodeError
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
@ -260,7 +261,7 @@ class User:
|
|||||||
The default user profile picture.
|
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)
|
return Image.open(image_path)
|
||||||
|
|
||||||
|
|
||||||
@ -304,6 +305,39 @@ class User:
|
|||||||
self.picture = User._get_default_picture()
|
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:
|
def get_dn(self) -> str:
|
||||||
"""Returns the LDAP DN for the DIT entry representing this User.
|
"""Returns the LDAP DN for the DIT entry representing this User.
|
||||||
|
|
||||||
|