diff --git a/CHANGES.rst b/CHANGES.rst
index cb5f8e7d..18cdfbc0 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,7 @@
+Added
+^^^^^
+- Group member removal can be achieved from the group edition page :issue:`192`
+
Changed
^^^^^^^
- Model `identifier_attributes` are fixed.
diff --git a/canaille/app/forms.py b/canaille/app/forms.py
index 679ed42a..6f40a7f5 100644
--- a/canaille/app/forms.py
+++ b/canaille/app/forms.py
@@ -251,12 +251,15 @@ def set_writable(field):
class IDToModel:
- def __init__(self, model_name):
+ def __init__(self, model_name, raise_on_errors=True):
self.model_name = model_name
+ self.raise_on_errors = raise_on_errors
def __call__(self, data):
model = getattr(models, self.model_name)
instance = data if isinstance(data, model) else model.get(data)
- if not instance:
+ if instance:
+ return instance
+
+ if self.raise_on_errors:
raise wtforms.ValidationError()
- return instance
diff --git a/canaille/core/endpoints/forms.py b/canaille/core/endpoints/forms.py
index b41a74a3..ad61d59d 100644
--- a/canaille/core/endpoints/forms.py
+++ b/canaille/core/endpoints/forms.py
@@ -15,6 +15,7 @@ from canaille.app.forms import is_uri
from canaille.app.forms import phone_number
from canaille.app.forms import set_readonly
from canaille.app.forms import unique_values
+from canaille.app.i18n import gettext
from canaille.app.i18n import lazy_gettext as _
from canaille.app.i18n import native_language_name_from_code
from canaille.backends import BaseBackend
@@ -56,6 +57,20 @@ def existing_login(form, field):
)
+def existing_group_member(form, field):
+ if field.data is None:
+ raise wtforms.ValidationError(
+ gettext("The user you are trying to remove does not exist.")
+ )
+
+ if field.data not in form.group.members:
+ raise wtforms.ValidationError(
+ gettext(
+ "The user '{user}' has already been removed from the group '{group}'"
+ ).format(user=field.data.formatted_name, group=form.group.display_name)
+ )
+
+
def non_empty_groups(form, field):
"""LDAP groups cannot be empty because groupOfNames.member is a MUST
attribute.
@@ -373,6 +388,13 @@ class EditGroupForm(Form):
)
+class DeleteGroupMemberForm(Form):
+ member = wtforms.StringField(
+ filters=[IDToModel("User", raise_on_errors=False)],
+ validators=[existing_group_member],
+ )
+
+
class JoinForm(Form):
email = wtforms.EmailField(
_("Email address"),
diff --git a/canaille/core/endpoints/groups.py b/canaille/core/endpoints/groups.py
index 5ee30b25..d92e3f9d 100644
--- a/canaille/core/endpoints/groups.py
+++ b/canaille/core/endpoints/groups.py
@@ -13,6 +13,7 @@ from canaille.app.i18n import gettext as _
from canaille.app.themes import render_template
from .forms import CreateGroupForm
+from .forms import DeleteGroupMemberForm
from .forms import EditGroupForm
bp = Blueprint("groups", __name__, url_prefix="/groups")
@@ -72,6 +73,12 @@ def group(user, group):
if request.form.get("action") == "delete":
return delete_group(group)
+ if request.form.get("action") == "confirm-remove-member":
+ return delete_member(group)
+
+ if request.form.get("action") == "remove-member":
+ return delete_member(group)
+
abort(400, f"bad form action: {request.form.get('action')}")
@@ -88,7 +95,11 @@ def edit_group(group):
},
)
- if request.form and not request.form.get("page"):
+ if (
+ request.form
+ and request.form.get("action") == "edit"
+ and not request.form.get("page")
+ ):
if form.validate():
group.description = form.description.data
group.save()
@@ -105,7 +116,7 @@ def edit_group(group):
return render_htmx_template(
"group.html",
- "partial/users.html",
+ "partial/group-members.html",
form=form,
menuitem="groups",
edited_group=group,
@@ -113,6 +124,36 @@ def edit_group(group):
)
+def delete_member(group):
+ form = DeleteGroupMemberForm(request.form or None)
+ form.group = group
+
+ if not form.validate():
+ flash(
+ "\n".join(form.errors.get("member")),
+ "error",
+ )
+
+ elif request.form.get("action") == "confirm-remove-member":
+ return render_template(
+ "modals/remove-group-member.html", group=group, form=form
+ )
+
+ else:
+ flash(
+ _(
+ f"{form.member.data.formatted_name} has been removed from the group {group.display_name}"
+ ),
+ "success",
+ )
+ group.members = [
+ member for member in group.members if member != form.member.data
+ ]
+ group.save()
+
+ return edit_group(group)
+
+
def delete_group(group):
flash(
_("The group %(group)s has been sucessfully deleted", group=group.display_name),
diff --git a/canaille/core/templates/group.html b/canaille/core/templates/group.html
index 33b6269b..8f01e3f9 100644
--- a/canaille/core/templates/group.html
+++ b/canaille/core/templates/group.html
@@ -80,7 +80,7 @@
{{ table.search(table_form, "table.users") }}
- {% include "partial/users.html" %}
+ {% include "partial/group-members.html" %}
{% endif %}
{% endblock %}
diff --git a/canaille/core/templates/modals/remove-group-member.html b/canaille/core/templates/modals/remove-group-member.html
new file mode 100644
index 00000000..f749a7ec
--- /dev/null
+++ b/canaille/core/templates/modals/remove-group-member.html
@@ -0,0 +1,31 @@
+{% extends theme('base.html') %}
+{% import 'macro/form.html' as fui %}
+
+{% block content %}
+
+ {% call fui.render_form(form) %}
+
+
+
+ {% trans %}Group member deletion{% endtrans %}
+
+
+
+ {% trans group_name=group.display_name, user_name=form.member.data.formatted_name %}
+ Are you sure you want to remove {{ user_name }} from the group "{{ group_name }}"?
+ {% endtrans %}
+