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/ldap.py:/app/lumi2/ldap.py:ro
|
||||||
- ./lumi2/usermodel.py:/app/lumi2/usermodel.py:ro
|
- ./lumi2/usermodel.py:/app/lumi2/usermodel.py:ro
|
||||||
- ./lumi2/usermanager.py:/app/lumi2/usermanager.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/css:/app/lumi2/static/css:ro
|
||||||
- ./lumi2/static/images/base:/app/lumi2/static/images/base:ro
|
- ./lumi2/static/images/base:/app/lumi2/static/images/base:ro
|
||||||
- ./lumi2/static/images/default:/app/lumi2/static/images/default: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.register_blueprint(usermanager.bp)
|
||||||
app.add_url_rule('/', endpoint='index')
|
app.add_url_rule('/', endpoint='index')
|
||||||
|
|
||||||
|
# TODO create OUs
|
||||||
|
# TODO create static files
|
||||||
|
|
||||||
return app
|
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 %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h1>Editing group: {{ group.groupname }}</h1>
|
<h1>Editing group: {{ groupname }}</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="post">
|
<div class="row">
|
||||||
{{ form.csrf_token }}
|
<div class="col-sm border rounded m-2">
|
||||||
<div class="row mb-3">
|
<table class="table table-hover align-middle" id="groupNonMembers">
|
||||||
<div class="col-sm">
|
<thead>
|
||||||
{{ form.members.label(class="form-label") }}
|
<tr>
|
||||||
{{ form.members(class="form-control" + (" is-invalid" if form.members.errors else "")) }}
|
<th scope="col" colspan="3" class="text-center">Other users</th>
|
||||||
{% if form.members.errors %}
|
</tr>
|
||||||
{% for error in form.members.errors %}
|
</thead>
|
||||||
<div class="invalid-feedback">{{ error }}</div>
|
<tbody>
|
||||||
{% endfor %}
|
{% for user in non_members %}
|
||||||
{% endif %}
|
<tr class="userEntry text-center" id="{{ user.username }}">
|
||||||
<div id="membersHelp" class="form-text">
|
<th scope="row">
|
||||||
List of members of this group.
|
<img src="{{ url_for('static', filename='images/users/' + user.username + '/thumbnail.jpg') }}"
|
||||||
</div>
|
alt="Profile picture for user {{ user.username }}"
|
||||||
</div>
|
class="img-fluid rounded"
|
||||||
<div class="col-sm">
|
style="max-width: 50px"
|
||||||
{{ form.non_members.label(class="form-label") }}
|
>
|
||||||
{{ form.non_members(class="form-control" + (" is-invalid" if form.non_members.errors else "")) }}
|
</th>
|
||||||
{% if form.non_members.errors %}
|
<td>
|
||||||
{% for error in form.non_members.errors %}
|
<a href="{{ url_for('usermanager.user_view', username=user.username)}}">{{ user.username }}</a>
|
||||||
<div class="invalid-feedback">{{ error }}</div>
|
</td>
|
||||||
{% endfor %}
|
<td>
|
||||||
{% endif %}
|
<button type="button" class="toggleMembershipButton btn btn-success">
|
||||||
<div id="non_membersHelp" class="form-text">
|
Add
|
||||||
List of users who are not members of this group.
|
</button>
|
||||||
</div>
|
</td>
|
||||||
</div>
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-3">
|
<div class="col-sm border rounded m-2">
|
||||||
<div class="col-sm">
|
<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"
|
<a class="btn btn-secondary"
|
||||||
href="#"
|
href="{{ url_for('usermanager.group_update', groupname=groupname) }}"
|
||||||
role="button">Reset</a>
|
role="button">Reset</a>
|
||||||
{{ form.submit(class_="btn btn-primary") }}
|
{{ form.updated_members }}
|
||||||
</div>
|
{{ form.submit(class="btn btn-primary") }}
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
|
<script src="{{ url_for('static', filename='js/groupEdit.js') }}"></script>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
href="{{ url_for('usermanager.user_create') }}"
|
href="{{ url_for('usermanager.user_create') }}"
|
||||||
role="button"
|
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
|
Create a new user
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryFile
|
from tempfile import TemporaryFile
|
||||||
|
from json import loads, dumps, JSONDecodeError
|
||||||
|
|
||||||
from flask import (
|
from flask import (
|
||||||
Blueprint, render_template, abort, request, flash, redirect, url_for, current_app
|
Blueprint, render_template, abort, request, flash, redirect, url_for, current_app
|
||||||
@ -9,7 +10,9 @@ from flask import (
|
|||||||
from PIL import Image, UnidentifiedImageError
|
from PIL import Image, UnidentifiedImageError
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from flask_wtf.file import FileField, FileAllowed
|
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
|
from wtforms.validators import InputRequired, Email, EqualTo
|
||||||
|
|
||||||
import lumi2.ldap as ldap
|
import lumi2.ldap as ldap
|
||||||
@ -166,7 +169,7 @@ class UserCreationForm(UserUpdateForm):
|
|||||||
submit = SubmitField(
|
submit = SubmitField(
|
||||||
'Create',
|
'Create',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/users/create", methods=("GET", "POST"))
|
@bp.route("/users/create", methods=("GET", "POST"))
|
||||||
@ -303,21 +306,15 @@ def user_delete(username: str):
|
|||||||
|
|
||||||
|
|
||||||
class GroupUpdateForm(FlaskForm):
|
class GroupUpdateForm(FlaskForm):
|
||||||
members = SelectMultipleField(
|
updated_members = HiddenField(
|
||||||
'Group members',
|
'Group Members',
|
||||||
validators=[InputRequired()],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
non_members = SelectMultipleField(
|
|
||||||
'Other users',
|
|
||||||
)
|
|
||||||
|
|
||||||
submit = SubmitField(
|
submit = SubmitField(
|
||||||
'Update',
|
'Apply',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/groups/update/<string:groupname>")
|
@bp.route("/groups/update/<string:groupname>", methods=("GET", "POST"))
|
||||||
def group_update(groupname: str):
|
def group_update(groupname: str):
|
||||||
"""Detail and Update view for a group.
|
"""Detail and Update view for a group.
|
||||||
|
|
||||||
@ -335,15 +332,39 @@ def group_update(groupname: str):
|
|||||||
conn.unbind()
|
conn.unbind()
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
members = {user for user in group.members}
|
||||||
|
non_members = {user for user in ldap.get_users(conn)} - members
|
||||||
form = GroupUpdateForm()
|
form = GroupUpdateForm()
|
||||||
form.members.choices = sorted({user.username for user in group.members})
|
|
||||||
form.non_members.choices = sorted(
|
if request.method == 'GET':
|
||||||
{user.username for user in ldap.get_users(conn)} - set(form.members.choices)
|
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()
|
conn.unbind()
|
||||||
return render_template(
|
return render_template(
|
||||||
'usermanager/group_edit.html',
|
'usermanager/group_edit.html',
|
||||||
form=form,
|
form=form,
|
||||||
group=group,
|
groupname=group.groupname,
|
||||||
|
members=members,
|
||||||
|
non_members=non_members,
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user