feat(usermanager): add group update view

This commit is contained in:
Julian Lobbes 2022-11-20 16:55:15 +01:00
parent 5c56e2d1de
commit a082af09c3
6 changed files with 237 additions and 52 deletions

View File

@ -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

View File

@ -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

View File

@ -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));
}
}

View File

@ -3,44 +3,82 @@
{% block content %}
<div class="row">
<div class="col">
<h1>Editing group: {{ group.groupname }}</h1>
<h1>Editing group: {{ groupname }}</h1>
</div>
</div>
<form method="post">
{{ form.csrf_token }}
<div class="row mb-3">
<div class="col-sm">
{{ 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 %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
{% endif %}
<div id="membersHelp" class="form-text">
List of members of this group.
</div>
</div>
<div class="col-sm">
{{ 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 %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
{% endif %}
<div id="non_membersHelp" class="form-text">
List of users who are not members of this group.
</div>
</div>
<div class="row">
<div class="col-sm border rounded m-2">
<table class="table table-hover align-middle" id="groupNonMembers">
<thead>
<tr>
<th scope="col" colspan="3" class="text-center">Other users</th>
</tr>
</thead>
<tbody>
{% for user in non_members %}
<tr class="userEntry text-center" id="{{ user.username }}">
<th scope="row">
<img src="{{ url_for('static', filename='images/users/' + user.username + '/thumbnail.jpg') }}"
alt="Profile picture for user {{ user.username }}"
class="img-fluid rounded"
style="max-width: 50px"
>
</th>
<td>
<a href="{{ url_for('usermanager.user_view', username=user.username)}}">{{ user.username }}</a>
</td>
<td>
<button type="button" class="toggleMembershipButton btn btn-success">
Add
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="row mb-3">
<div class="col-sm">
<div class="col-sm border rounded m-2">
<table class="table table-hover align-middle" id="groupMembers">
<thead>
<tr>
<th scope="col" colspan="3" class="text-center">{{ groupname }}</th>
</tr>
</thead>
<tbody>
{% for user in members %}
<tr class="userEntry text-center" id="{{ user.username }}">
<th scope="row">
<img src="{{ url_for('static', filename='images/users/' + user.username + '/thumbnail.jpg') }}"
alt="Profile picture for user {{ user.username }}"
class="img-fluid rounded"
style="max-width: 50px"
>
</th>
<td>
<a href="{{ url_for('usermanager.user_view', username=user.username)}}">{{ user.username }}</a>
</td>
<td>
<button type="button" class="toggleMembershipButton btn btn-danger">
Remove
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="row mb-2">
<div class="d-grid gap-2 col-2 mx-auto">
<form method="post" enctype="application/json">
{{ form.csrf_token }}
<a class="btn btn-secondary"
href="#"
href="{{ url_for('usermanager.group_update', groupname=groupname) }}"
role="button">Reset</a>
{{ form.submit(class_="btn btn-primary") }}
</div>
{{ form.updated_members }}
{{ form.submit(class="btn btn-primary") }}
</form>
</div>
</form>
</div>
<script src="{{ url_for('static', filename='js/groupEdit.js') }}"></script>
{% endblock content %}

View File

@ -9,7 +9,7 @@
href="{{ url_for('usermanager.user_create') }}"
role="button"
>
<img src="/static/images/base/plus.png" alt="Plus-Icon" width="16" height="16">
<img src="{{ url_for('static', filename='/images/base/plus.png') }}" alt="Plus-Icon" width="16" height="16">
Create a new user
</a>
</div>

View File

@ -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/<string:groupname>")
@bp.route("/groups/update/<string:groupname>", 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,
)