diff --git a/lumi2/static/images/base/navbar-logo.svg b/lumi2/static/images/base/navbar-logo.svg new file mode 100644 index 0000000..08fcadc --- /dev/null +++ b/lumi2/static/images/base/navbar-logo.svg @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + LUMI 2 + + diff --git a/lumi2/static/images/base/plus-circle.png b/lumi2/static/images/base/plus-circle.png new file mode 100644 index 0000000..9d72f57 Binary files /dev/null and b/lumi2/static/images/base/plus-circle.png differ diff --git a/lumi2/static/images/base/plus-circle.svg b/lumi2/static/images/base/plus-circle.svg new file mode 100644 index 0000000..66308ef --- /dev/null +++ b/lumi2/static/images/base/plus-circle.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/lumi2/static/images/base/plus.png b/lumi2/static/images/base/plus.png new file mode 100644 index 0000000..961da2f Binary files /dev/null and b/lumi2/static/images/base/plus.png differ diff --git a/lumi2/static/images/base/plus.svg b/lumi2/static/images/base/plus.svg new file mode 100644 index 0000000..5b088c0 --- /dev/null +++ b/lumi2/static/images/base/plus.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/lumi2/templates/base.html b/lumi2/templates/base.html index 4379ccb..7b6d7f1 100644 --- a/lumi2/templates/base.html +++ b/lumi2/templates/base.html @@ -23,14 +23,39 @@ + + {% with messages = get_flashed_messages() %} {% if messages %} {% for message in messages %} -
{{ message }}
+
{{ message }}
{% endfor %} {% endif %} {% endwith %} -
+
{% block content %} {% endblock content %}
diff --git a/lumi2/templates/usermanager/user_update.html b/lumi2/templates/usermanager/user_edit.html similarity index 68% rename from lumi2/templates/usermanager/user_update.html rename to lumi2/templates/usermanager/user_edit.html index 29b023e..6a4f250 100644 --- a/lumi2/templates/usermanager/user_update.html +++ b/lumi2/templates/usermanager/user_edit.html @@ -3,11 +3,22 @@ {% block content %}
-

Edit user: {{ username }}

+

{{ heading }}

{{ form.csrf_token }} + {% if not is_update %} +
+ {{ form.username.label(class="form-label") }} + {{ form.username(class="form-control" + (" is-invalid" if form.username.errors else "")) }} + {% if form.username.errors %} + {% for error in form.username.errors %} +
{{ error }}
+ {% endfor %} + {% endif %} +
+ {% endif %}
{{ form.email.label(class="form-label") }} {{ form.email(class="form-control" + (" is-invalid" if form.email.errors else "")) }} @@ -43,6 +54,11 @@
{{ error }}
{% endfor %} {% endif %} + {% if not is_update %} +
+ Leave empty to use the first name. Will be displayed instead of the first name in some applications. +
+ {% endif %}
{{ form.password.label(class="form-label") }} @@ -52,6 +68,13 @@
{{ error }}
{% endfor %} {% endif %} +
+ {% if is_update %} + Must be at least 8 characters long. Leave empty to keep the current password. + {% else %} + Must be at least 8 characters long. + {% endif %} +
{{ form.password_confirmation.label(class="form-label") }} @@ -70,11 +93,24 @@
{{ error }}
{% endfor %} {% endif %} +
+ {% if is_update %} + Only JPEG files can be used. Leave empty to keep the current picture. + {% else %} + Optional but recommended. Only JPEG files can be used. + {% endif %} +
+ {% if is_update %} Cancel + {% else %} + Cancel + {% endif %} {{ form.submit(class_="btn btn-primary") }}
diff --git a/lumi2/templates/usermanager/user_list.html b/lumi2/templates/usermanager/user_list.html index e031691..91d1e61 100644 --- a/lumi2/templates/usermanager/user_list.html +++ b/lumi2/templates/usermanager/user_list.html @@ -1,34 +1,46 @@ {% extends 'base.html' %} {% block content %} +
+

All users

+
+
+ + Plus-Icon + Create a new user + +
- - + + - {% for user in users %} + {% for user in users | sort %} - - + + {% endfor %} diff --git a/lumi2/templates/usermanager/user_test.html b/lumi2/templates/usermanager/user_test.html deleted file mode 100644 index 2392874..0000000 --- a/lumi2/templates/usermanager/user_test.html +++ /dev/null @@ -1,16 +0,0 @@ -{% extends 'base.html' %} - -{% block content %} -
-
-
-
- Column -
-
- Column -
-
-
-
-{% endblock content %} diff --git a/lumi2/templates/usermanager/user_view.html b/lumi2/templates/usermanager/user_view.html index f8e4491..7387093 100644 --- a/lumi2/templates/usermanager/user_view.html +++ b/lumi2/templates/usermanager/user_view.html @@ -1,45 +1,28 @@ {% extends 'base.html' %} {% block content %} -
-
+
+
profile picture for user {{ user.username }}
-
-
-
-

{{ user.username }}

+
+
Username: {{ user.username }}
+
Email: {{ user.email }}
+
First Name: {{ user.first_name }}
+
Last Name: {{ user.last_name }}
+
Nickname: {{ user.display_name }}
-
-
-
Username:
-
{{ user.username }}
-
-
-
Email:
-
{{ user.email }}
-
-
-
First Name:
-
{{ user.first_name }}
-
-
-
Last Name:
-
{{ user.last_name }}
-
-
-
Nickname:
-
{{ user.display_name }}
-
-
-
+
{% endblock content %} diff --git a/lumi2/usermanager.py b/lumi2/usermanager.py index ff128bd..cfc39e9 100644 --- a/lumi2/usermanager.py +++ b/lumi2/usermanager.py @@ -4,7 +4,7 @@ from pathlib import Path from tempfile import TemporaryFile from flask import ( - Blueprint, render_template, abort, request, flash, redirect, url_for + Blueprint, render_template, abort, request, flash, redirect, url_for, current_app ) from PIL import Image, UnidentifiedImageError from flask_wtf import FlaskForm @@ -68,20 +68,14 @@ def user_list(): users=users, ) - -@bp.route("/users/test") -def user_test(): - return render_template( - 'usermanager/user_test.html', - ) - class UserUpdateForm(FlaskForm): @staticmethod def validate_name(form, field) -> None: - try: - User.assert_is_valid_name(field.data) - except InvalidStringFormatException as e: - raise ValidationError(str(e)) + if field.data: + try: + User.assert_is_valid_name(field.data) + except InvalidStringFormatException as e: + raise ValidationError(str(e)) @staticmethod def validate_password(form, field) -> None: @@ -115,11 +109,11 @@ class UserUpdateForm(FlaskForm): [InputRequired(), validate_name] ) display_name = StringField( - 'Nick Name', + 'Nickname', [InputRequired(), validate_name] ) password = PasswordField( - 'Password (leave empty to keep the same)', + 'Password', [ EqualTo('password_confirmation', message='Passwords must match'), validate_password, @@ -137,6 +131,82 @@ class UserUpdateForm(FlaskForm): ) +class UserCreationForm(UserUpdateForm): + @staticmethod + def validate_username(form, field) -> None: + try: + User.assert_is_valid_username(field.data) + except InvalidStringFormatException as e: + raise ValidationError(str(e)) + new_user_dn = f"uid={field.data}," + current_app.config['LDAP_USERS_OU'] + conn = ldap.get_connection() + if ldap.user_exists(conn, new_user_dn): + raise ValidationError("Username is taken.") + conn.unbind() + + username = StringField( + 'Username', + [ + InputRequired('Please enter a username.'), + validate_username + ] + ) + display_name = StringField( + 'Nickname', + [UserUpdateForm.validate_name] + ) + password = PasswordField( + 'Password', + [ + EqualTo('password_confirmation', message='Passwords must match'), + InputRequired('Please enter a password.'), + UserUpdateForm.validate_password, + ], + ) + submit = SubmitField( + 'Create', + ) + + + +@bp.route("/users/create", methods=("GET", "POST")) +def user_create(): + """Creation view for a new User. + + Provides a form which can be used to enter the new user's details. + """ + try: + conn = ldap.get_connection() + except Exception: + abort(500) + + form = UserCreationForm() + if form.validate_on_submit(): + user = User( + form.username.data, + User.generate_password_hash(form.password.data), + form.email.data, + form.first_name.data, + form.last_name.data, + form.display_name.data if form.display_name.data else None, + Image.open(form.picture.data, formats=['JPEG']) if form.picture.data and form.picture.data.filename else None, + ) + + ldap.create_user(conn, user) + user._generate_static_images(force=True) + conn.unbind() + flash(f"User '{user.username}' was created.") + return redirect(url_for('usermanager.user_view', username=user.username)) + + conn.unbind() + return render_template( + 'usermanager/user_edit.html', + form=form, + heading=f"Create a new user", + is_update=False, + ) + + @bp.route("/users/update/", methods=("GET", "POST")) def user_update(username: str): """Update view for a specific User. @@ -183,7 +253,9 @@ def user_update(username: str): conn.unbind() return render_template( - 'usermanager/user_update.html', + 'usermanager/user_edit.html', form=form, - username=user.username + username=user.username, + heading=f"Edit user: {user.username}", + is_update=True, )
Picture UsernameEmail addressFirst Name Last NameFirst NameEmail address Nickname
profile picture for user {{ user.username }} {{ user.username }} {{ user.email }}{{ user.first_name }} {{ user.last_name }}{{ user.first_name }}{{ user.email }} {{ user.display_name }}