feat(usermanager): implement User update and detail views
This commit is contained in:
parent
cd7233f566
commit
b111490bab
@ -1,38 +1,39 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
<title>{{ config.SITE_TITLE }}</title>
|
<title>{{ config.SITE_TITLE }}</title>
|
||||||
<meta name="description" content="{{ config.SITE_DESCRIPTION }}">
|
<meta name="description" content="{{ config.SITE_DESCRIPTION }}">
|
||||||
<meta name="author" content="{{ config.SITE_AUTHOR }}">
|
<meta name="author" content="{{ config.SITE_AUTHOR }}">
|
||||||
|
|
||||||
<meta property="og:title" content="{{ config.SITE_TITLE }}">
|
<meta property="og:title" content="{{ config.SITE_TITLE }}">
|
||||||
<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/base/og.png') }}">
|
<meta property="og:image" content="{{ url_for('static', filename='images/base/og.png') }}">
|
||||||
|
|
||||||
<link rel="icon" href="{{ url_for('static', filename='images/base/favicon.ico') }}">
|
<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="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="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>
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
|
||||||
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{% with messages = get_flashed_messages() %}
|
{% with messages = get_flashed_messages() %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
<ul class=flash-message>
|
{% for message in messages %}
|
||||||
{% for message in messages %}
|
<div class="alert alert-primary m-3">{{ message }}</div>
|
||||||
<li>{{ message }}</li>
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endif %}
|
||||||
</ul>
|
{% endwith %}
|
||||||
{% endif %}
|
<div class="container border rounded">
|
||||||
{% endwith %}
|
{% block content %}
|
||||||
{% block content %}
|
{% endblock content %}
|
||||||
{% endblock content %}
|
</div>
|
||||||
</body>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,32 +1,81 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>User: {{ user.username }}</h1>
|
<div class="row">
|
||||||
<img src="{{ url_for('static', filename='images/users/' + user.username + '/full.jpg') }}" alt="profile picture for user {{ user.username }}">
|
<div class="col">
|
||||||
|
<h1>Edit user: {{ username }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<form method="post" enctype="multipart/form-data">
|
<form method="post" enctype="multipart/form-data">
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
|
<div class="mb-3">
|
||||||
{{ form.email.label }}
|
{{ form.email.label(class="form-label") }}
|
||||||
{{ form.email }}
|
{{ form.email(class="form-control" + (" is-invalid" if form.email.errors else "")) }}
|
||||||
|
{% if form.email.errors %}
|
||||||
{{ form.first_name.label }}
|
{% for error in form.email.errors %}
|
||||||
{{ form.first_name }}
|
<div class="invalid-feedback">{{ error }}</div>
|
||||||
|
{% endfor %}
|
||||||
{{ form.last_name.label }}
|
{% endif %}
|
||||||
{{ form.last_name }}
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
{{ form.display_name.label }}
|
{{ form.first_name.label(class="form-label") }}
|
||||||
{{ form.display_name }}
|
{{ form.first_name(class="form-control" + (" is-invalid" if form.first_name.errors else "")) }}
|
||||||
|
{% if form.first_name.errors %}
|
||||||
{{ form.password.label }}
|
{% for error in form.first_name.errors %}
|
||||||
{{ form.password }}
|
<div class="invalid-feedback">{{ error }}</div>
|
||||||
|
{% endfor %}
|
||||||
{{ form.password_confirmation.label }}
|
{% endif %}
|
||||||
{{ form.password_confirmation }}
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
{{ form.picture.label }}
|
{{ form.last_name.label(class="form-label") }}
|
||||||
{{ form.picture }}
|
{{ form.last_name(class="form-control" + (" is-invalid" if form.last_name.errors else "")) }}
|
||||||
|
{% if form.last_name.errors %}
|
||||||
{{ form.submit }}
|
{% for error in form.last_name.errors %}
|
||||||
|
<div class="invalid-feedback">{{ error }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.display_name.label(class="form-label") }}
|
||||||
|
{{ form.display_name(class="form-control" + (" is-invalid" if form.display_name.errors else "")) }}
|
||||||
|
{% if form.display_name.errors %}
|
||||||
|
{% for error in form.display_name.errors %}
|
||||||
|
<div class="invalid-feedback">{{ error }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.password.label(class="form-label") }}
|
||||||
|
{{ form.password(class="form-control" + (" is-invalid" if form.password.errors else "")) }}
|
||||||
|
{% if form.password.errors %}
|
||||||
|
{% for error in form.password.errors %}
|
||||||
|
<div class="invalid-feedback">{{ error }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.password_confirmation.label(class="form-label") }}
|
||||||
|
{{ form.password_confirmation(class="form-control" + (" is-invalid" if form.password_confirmation.errors else "")) }}
|
||||||
|
{% if form.password_confirmation.errors %}
|
||||||
|
{% for error in form.password_confirmation.errors %}
|
||||||
|
<div class="invalid-feedback">{{ error }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.picture.label(class="form-label") }}
|
||||||
|
{{ form.picture(class="form-control" + (" is-invalid" if form.picture.errors else "")) }}
|
||||||
|
{% if form.picture.errors %}
|
||||||
|
{% for error in form.picture.errors %}
|
||||||
|
<div class="invalid-feedback">{{ error }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<a class="btn btn-secondary"
|
||||||
|
href="{{ url_for('usermanager.user_view', username=username) }}"
|
||||||
|
role="button">Cancel</a>
|
||||||
|
{{ form.submit(class_="btn btn-primary") }}
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
44
lumi2/templates/usermanager/user_view.html
Normal file
44
lumi2/templates/usermanager/user_view.html
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row text-center">
|
||||||
|
<div class="col">
|
||||||
|
<img src="{{ url_for('static', filename='images/users/' + user.username + '/full.jpg') }}"
|
||||||
|
alt="profile picture for user {{ user.username }}"
|
||||||
|
class="img-thumbnail"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row text-center">
|
||||||
|
<div class="col">
|
||||||
|
<h1>{{ user.username }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row gx-1">
|
||||||
|
<div class="col text-end fw-bold">Username:</div>
|
||||||
|
<div class="col text-start">{{ user.username }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row gx-1">
|
||||||
|
<div class="col text-end fw-bold">Email:</div>
|
||||||
|
<div class="col text-start">{{ user.email }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row gx-1">
|
||||||
|
<div class="col text-end fw-bold">First Name:</div>
|
||||||
|
<div class="col text-start">{{ user.first_name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row gx-1">
|
||||||
|
<div class="col text-end fw-bold">Last Name:</div>
|
||||||
|
<div class="col text-start">{{ user.last_name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row gx-1">
|
||||||
|
<div class="col text-end fw-bold">Nickname:</div>
|
||||||
|
<div class="col text-start">{{ user.display_name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row text-center">
|
||||||
|
<div class="col">
|
||||||
|
<a class="btn btn-primary"
|
||||||
|
href="{{ url_for('usermanager.user_update', username=user.username) }}"
|
||||||
|
role="button">Edit</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
@ -1,10 +1,10 @@
|
|||||||
"""Views for lumi2."""
|
"""Views for lumi2."""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryFile
|
||||||
|
|
||||||
from flask import (
|
from flask import (
|
||||||
Blueprint, render_template, abort, request, flash, redirect
|
Blueprint, render_template, abort, request, flash, redirect, url_for
|
||||||
)
|
)
|
||||||
from PIL import Image, UnidentifiedImageError
|
from PIL import Image, UnidentifiedImageError
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
@ -27,31 +27,55 @@ def index():
|
|||||||
return render_template('usermanager/index.html')
|
return render_template('usermanager/index.html')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/user/view/<string:username>")
|
||||||
|
def user_view(username: str):
|
||||||
|
"""Detail view for a specific User.
|
||||||
|
|
||||||
|
Shows the user's information.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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()
|
||||||
|
conn.unbind()
|
||||||
|
return render_template('usermanager/user_view.html',user=user)
|
||||||
|
|
||||||
|
|
||||||
class UserUpdateForm(FlaskForm):
|
class UserUpdateForm(FlaskForm):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def validate_name(form, field) -> None:
|
def validate_name(form, field) -> None:
|
||||||
try:
|
try:
|
||||||
User.assert_is_valid_name(field.data)
|
User.assert_is_valid_name(field.data)
|
||||||
except InvalidStringFormatException as e:
|
except InvalidStringFormatException as e:
|
||||||
raise ValidationError from e
|
raise ValidationError(str(e))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def validate_password(form, field) -> None:
|
def validate_password(form, field) -> None:
|
||||||
try:
|
if field.data:
|
||||||
User.assert_is_valid_password(field.data)
|
try:
|
||||||
except InvalidStringFormatException as e:
|
User.assert_is_valid_password(field.data)
|
||||||
raise ValidationError from e
|
except InvalidStringFormatException as e:
|
||||||
|
raise ValidationError(str(e))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def validate_picture(form, field) -> None:
|
def validate_picture(form, field) -> None:
|
||||||
if field.data:
|
if field.data and field.data.filename:
|
||||||
try:
|
try:
|
||||||
with TemporaryDirectory() as temp_dir:
|
Image.open(field.data, formats=['JPEG'])
|
||||||
temp_file = Path(temp_dir) / "upload.jpg"
|
field.data.seek(0)
|
||||||
field.data.save(temp_file)
|
|
||||||
Image.open(temp_file, formats=['JPEG'])
|
|
||||||
except UnidentifiedImageError as e:
|
except UnidentifiedImageError as e:
|
||||||
raise ValidationError from e
|
raise ValidationError(
|
||||||
|
"Invalid JPEG file. It may be corrupted."
|
||||||
|
)
|
||||||
|
|
||||||
email = StringField(
|
email = StringField(
|
||||||
'Email',
|
'Email',
|
||||||
@ -70,7 +94,7 @@ class UserUpdateForm(FlaskForm):
|
|||||||
[InputRequired(), validate_name]
|
[InputRequired(), validate_name]
|
||||||
)
|
)
|
||||||
password = PasswordField(
|
password = PasswordField(
|
||||||
'Password',
|
'Password (leave empty to keep the same)',
|
||||||
[
|
[
|
||||||
EqualTo('password_confirmation', message='Passwords must match'),
|
EqualTo('password_confirmation', message='Passwords must match'),
|
||||||
validate_password,
|
validate_password,
|
||||||
@ -90,7 +114,10 @@ class UserUpdateForm(FlaskForm):
|
|||||||
|
|
||||||
@bp.route("/user/update/<string:username>", methods=("GET", "POST"))
|
@bp.route("/user/update/<string:username>", methods=("GET", "POST"))
|
||||||
def user_update(username: str):
|
def user_update(username: str):
|
||||||
"""Update view for a specific User."""
|
"""Update view for a specific User.
|
||||||
|
|
||||||
|
Provides a form which can be used to edit that user's details.
|
||||||
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
conn = ldap.get_connection()
|
conn = ldap.get_connection()
|
||||||
@ -105,41 +132,33 @@ def user_update(username: str):
|
|||||||
|
|
||||||
user._generate_static_images()
|
user._generate_static_images()
|
||||||
|
|
||||||
if request.method == 'GET':
|
form = UserUpdateForm(obj=user)
|
||||||
form = UserUpdateForm(obj=user)
|
if form.validate_on_submit():
|
||||||
else:
|
if form.email.data:
|
||||||
form = UserUpdateForm(request.form)
|
user.email = form.email.data
|
||||||
if form.validate():
|
if form.first_name.data:
|
||||||
if form.email.data:
|
user.first_name = form.first_name.data
|
||||||
user.email = form.email.data
|
if form.last_name.data:
|
||||||
print("Email updated.")
|
user.last_name = form.last_name.data
|
||||||
if form.first_name.data:
|
if form.display_name.data:
|
||||||
user.first_name = form.first_name.data
|
user.display_name = form.display_name.data
|
||||||
print("First Name updated.")
|
if form.password.data:
|
||||||
if form.last_name.data:
|
user.password_hash = User.generate_password_hash(form.password.data)
|
||||||
user.last_name = form.last_name.data
|
picture_updated = False
|
||||||
print("Last Name updated.")
|
if form.picture.data and form.picture.data.filename:
|
||||||
if form.display_name.data:
|
user.picture = Image.open(form.picture.data, formats=['JPEG'])
|
||||||
user.display_name = form.display_name.data
|
picture_updated = True
|
||||||
print("Display Name updated.")
|
|
||||||
if form.password.data:
|
|
||||||
user.password_hash = User.generate_password_hash(form.password.data)
|
|
||||||
print("Password updated.")
|
|
||||||
picture_updated = False
|
|
||||||
if form.picture.data:
|
|
||||||
with TemporaryDirectory() as temp_dir:
|
|
||||||
temp_file = Path(temp_dir) / "upload.jpg"
|
|
||||||
form.picture.data.save(temp_file)
|
|
||||||
user.picture = Image.open(temp_file, formats=['JPEG'])
|
|
||||||
picture_updated = True
|
|
||||||
print("Picture updated.")
|
|
||||||
|
|
||||||
ldap.update_user(conn, user)
|
ldap.update_user(conn, user)
|
||||||
if picture_updated:
|
if picture_updated:
|
||||||
user._generate_static_images(force=True)
|
user._generate_static_images(force=True)
|
||||||
conn.unbind()
|
conn.unbind()
|
||||||
# TODO redirect to user detail view
|
flash(f"Information for user '{user.username}' was updated.")
|
||||||
return redirect(request.url)
|
return redirect(url_for('usermanager.user_view', username=user.username))
|
||||||
|
|
||||||
conn.unbind()
|
conn.unbind()
|
||||||
return render_template('usermanager/user_update.html', form=form, user=user)
|
return render_template(
|
||||||
|
'usermanager/user_update.html',
|
||||||
|
form=form,
|
||||||
|
username=user.username
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user