feat(usermanager): add group update view
This commit is contained in:
parent
5c56e2d1de
commit
a082af09c3
@ -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
|
||||
|
@ -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
|
||||
|
122
lumi2/static/js/groupEdit.js
Normal file
122
lumi2/static/js/groupEdit.js
Normal 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));
|
||||
}
|
||||
}
|
@ -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 %}
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user