From a082af09c3867424d420f92d58c38637f8b43c1f Mon Sep 17 00:00:00 2001 From: Julian Lobbes Date: Sun, 20 Nov 2022 16:55:15 +0100 Subject: [PATCH] feat(usermanager): add group update view --- docker-compose.yml | 1 + lumi2/__init__.py | 3 + lumi2/static/js/groupEdit.js | 122 ++++++++++++++++++++ lumi2/templates/usermanager/group_edit.html | 106 +++++++++++------ lumi2/templates/usermanager/user_list.html | 2 +- lumi2/usermanager.py | 55 ++++++--- 6 files changed, 237 insertions(+), 52 deletions(-) create mode 100644 lumi2/static/js/groupEdit.js diff --git a/docker-compose.yml b/docker-compose.yml index aeca697..657bc61 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,7 @@ 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/js:/app/lumi2/static/js: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 diff --git a/lumi2/__init__.py b/lumi2/__init__.py index f9f63c3..46571eb 100644 --- a/lumi2/__init__.py +++ b/lumi2/__init__.py @@ -44,4 +44,7 @@ def create_app(test_config=None): app.register_blueprint(usermanager.bp) app.add_url_rule('/', endpoint='index') + # TODO create OUs + # TODO create static files + return app diff --git a/lumi2/static/js/groupEdit.js b/lumi2/static/js/groupEdit.js new file mode 100644 index 0000000..ba81020 --- /dev/null +++ b/lumi2/static/js/groupEdit.js @@ -0,0 +1,122 @@ +class AbstractUserEntry { + constructor(username, tableRow) { + this.username = username; + this.tableRow = tableRow; + this.membershipToggleButton = tableRow.querySelector(".toggleMembershipButton"); + this.membershipToggleButton.addEventListener("click", this.onButtonPress.bind(this)); + }; +}; + +function removeUserFromFormField(username) { + let formField = document.getElementById("updated_members"); + let oldMembersList = JSON.parse(formField.value); + let newMembersList = Array(); + for (let member of oldMembersList) { + if (member != username) { + newMembersList.push(member); + } + } + formField.value = JSON.stringify(newMembersList); +}; + +function addUserToFormField(username) { + let formField = document.getElementById("updated_members"); + let oldMembersList = JSON.parse(formField.value); + oldMembersList.push(username); + formField.value = JSON.stringify(oldMembersList); +}; + +class MemberEntry extends AbstractUserEntry { + onButtonPress() { + this.tableRow.remove(); + createRemovedMemberRow(this.username); + removeUserFromFormField(this.username); + }; +}; + +class NonMemberEntry extends AbstractUserEntry { + onButtonPress() { + this.tableRow.remove(); + createAddedMemberRow(this.username); + addUserToFormField(this.username); + }; +}; + +function createRemovedMemberRow(username) { + let newTableRow = nonMembersTable.querySelector("tbody").insertRow(0); + newTableRow.className = "userEntry text-center bg-danger bg-gradient"; + newTableRow.id = username; + + let newTableHeader = document.createElement("th"); + newTableHeader.scope = "row"; + let newImage = document.createElement("img"); + newImage.src = `/static/images/users/${username}/thumbnail.jpg`; + newImage.alt = `Profile picture for user ${username}.`; + newImage.className = "img-fluid rounded"; + newImage.style = "max-width: 50px"; + newTableHeader.appendChild(newImage); + newTableRow.appendChild(newTableHeader); + + let newTableDataUsername = document.createElement("td"); + let newUsernameAnchor = document.createElement("a"); + newUsernameAnchor.href = `/users/view/${username}`; + newUsernameAnchor.textContent = username; + newTableDataUsername.appendChild(newUsernameAnchor); + newTableRow.appendChild(newTableDataUsername); + + + let newTableDataButton = document.createElement("td"); + let newTableButton = document.createElement("button"); + newTableButton.type = "button"; + newTableButton.className = "toggleMembershipButton btn btn-outline-light"; + newTableButton.disabled = true; + newTableButton.textContent = "Being removed..."; + newTableDataButton.appendChild(newTableButton); + newTableRow.appendChild(newTableDataButton); +}; + +function createAddedMemberRow(username) { + let newTableRow = membersTable.querySelector("tbody").insertRow(0); + newTableRow.className = "userEntry text-center bg-success bg-gradient"; + newTableRow.id = username; + + let newTableHeader = document.createElement("th"); + newTableHeader.scope = "row"; + let newImage = document.createElement("img"); + newImage.src = `/static/images/users/${username}/thumbnail.jpg`; + newImage.alt = `Profile picture for user ${username}.`; + newImage.className = "img-fluid rounded"; + newImage.style = "max-width: 50px"; + newTableHeader.appendChild(newImage); + newTableRow.appendChild(newTableHeader); + + let newTableDataUsername = document.createElement("td"); + let newUsernameAnchor = document.createElement("a"); + newUsernameAnchor.href = `/users/view/${username}`; + newUsernameAnchor.textContent = username; + newTableDataUsername.appendChild(newUsernameAnchor); + newTableRow.appendChild(newTableDataUsername); + + + let newTableDataButton = document.createElement("td"); + let newTableButton = document.createElement("button"); + newTableButton.type = "button"; + newTableButton.className = "toggleMembershipButton btn btn-outline-light"; + newTableButton.disabled = true; + newTableButton.textContent = "Being added..."; + newTableDataButton.appendChild(newTableButton); + newTableRow.appendChild(newTableDataButton); +}; + +const membersTable = document.getElementById("groupMembers"); +const nonMembersTable = document.getElementById("groupNonMembers"); +let memberEntries = new Set(); +let nonMemberEntries = new Set(); + +for (let userEntry of document.body.querySelectorAll(".userEntry")) { + if (userEntry.parentElement.parentElement.id === "groupMembers") { + memberEntries.add(new MemberEntry(userEntry.id, userEntry)); + } else if (userEntry.parentElement.parentElement.id === "groupNonMembers") { + nonMemberEntries.add(new NonMemberEntry(userEntry.id, userEntry)); + } +} diff --git a/lumi2/templates/usermanager/group_edit.html b/lumi2/templates/usermanager/group_edit.html index 02a6832..564f9fd 100644 --- a/lumi2/templates/usermanager/group_edit.html +++ b/lumi2/templates/usermanager/group_edit.html @@ -3,44 +3,82 @@ {% block content %}
-

Editing group: {{ group.groupname }}

+

Editing group: {{ groupname }}

-
- {{ form.csrf_token }} -
-
- {{ form.members.label(class="form-label") }} - {{ form.members(class="form-control" + (" is-invalid" if form.members.errors else "")) }} - {% if form.members.errors %} - {% for error in form.members.errors %} -
{{ error }}
- {% endfor %} - {% endif %} -
- List of members of this group. -
-
-
- {{ form.non_members.label(class="form-label") }} - {{ form.non_members(class="form-control" + (" is-invalid" if form.non_members.errors else "")) }} - {% if form.non_members.errors %} - {% for error in form.non_members.errors %} -
{{ error }}
- {% endfor %} - {% endif %} -
- List of users who are not members of this group. -
-
+
+
+ + + + + + + + {% for user in non_members %} + + + + + + {% endfor %} + +
Other users
+ Profile picture for user {{ user.username }} + + {{ user.username }} + + +
-
-
+
+ + + + + + + + {% for user in members %} + + + + + + {% endfor %} + +
{{ groupname }}
+ Profile picture for user {{ user.username }} + + {{ user.username }} + + +
+
+
+
+
+ + {{ form.csrf_token }} Reset - {{ form.submit(class_="btn btn-primary") }} -
+ {{ form.updated_members }} + {{ form.submit(class="btn btn-primary") }} +
- +
+ {% endblock content %} diff --git a/lumi2/templates/usermanager/user_list.html b/lumi2/templates/usermanager/user_list.html index 33e4880..19f3333 100644 --- a/lumi2/templates/usermanager/user_list.html +++ b/lumi2/templates/usermanager/user_list.html @@ -9,7 +9,7 @@ href="{{ url_for('usermanager.user_create') }}" role="button" > - Plus-Icon + Plus-Icon Create a new user
diff --git a/lumi2/usermanager.py b/lumi2/usermanager.py index 8bbef5f..d235916 100644 --- a/lumi2/usermanager.py +++ b/lumi2/usermanager.py @@ -2,6 +2,7 @@ from pathlib import Path from tempfile import TemporaryFile +from json import loads, dumps, JSONDecodeError from flask import ( Blueprint, render_template, abort, request, flash, redirect, url_for, current_app @@ -9,7 +10,9 @@ from flask import ( from PIL import Image, UnidentifiedImageError from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileAllowed -from wtforms import ValidationError, StringField, PasswordField, SubmitField, SelectMultipleField +from wtforms import ( + ValidationError, StringField, PasswordField, SubmitField, HiddenField +) from wtforms.validators import InputRequired, Email, EqualTo import lumi2.ldap as ldap @@ -166,7 +169,7 @@ class UserCreationForm(UserUpdateForm): submit = SubmitField( 'Create', ) - + @bp.route("/users/create", methods=("GET", "POST")) @@ -303,21 +306,15 @@ def user_delete(username: str): class GroupUpdateForm(FlaskForm): - members = SelectMultipleField( - 'Group members', - validators=[InputRequired()], + updated_members = HiddenField( + 'Group Members', ) - - non_members = SelectMultipleField( - 'Other users', - ) - submit = SubmitField( - 'Update', + 'Apply', ) -@bp.route("/groups/update/") +@bp.route("/groups/update/", methods=("GET", "POST")) def group_update(groupname: str): """Detail and Update view for a group. @@ -335,15 +332,39 @@ def group_update(groupname: str): conn.unbind() abort(404) + members = {user for user in group.members} + non_members = {user for user in ldap.get_users(conn)} - members form = GroupUpdateForm() - form.members.choices = sorted({user.username for user in group.members}) - form.non_members.choices = sorted( - {user.username for user in ldap.get_users(conn)} - set(form.members.choices) - ) + + if request.method == 'GET': + form.updated_members.data = dumps([user.username for user in members]) + else: + try: + updated_usernames_list = loads(form.updated_members.data) + except JSONDecodeError: + abort(400) + if not isinstance(updated_usernames_list, list): + abort(400) + updated_members = set() + for username in updated_usernames_list: + if not isinstance(username, str): + abort(400) + try: + updated_members.add(ldap.get_user(conn, username)) + except ldap.EntryNotFoundException: + abort(400) + if not len(updated_members): + abort(400) + ldap.update_group(conn, Group(group.groupname, updated_members)) + flash(f"The Group '{group.groupname}' was updated.") + # TODO redirect to group list view + return redirect(url_for('usermanager.group_update', groupname=group.groupname)) conn.unbind() return render_template( 'usermanager/group_edit.html', form=form, - group=group, + groupname=group.groupname, + members=members, + non_members=non_members, )