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 %} + +{% endblock %} diff --git a/canaille/core/templates/partial/group-members.html b/canaille/core/templates/partial/group-members.html new file mode 100644 index 00000000..facc31f6 --- /dev/null +++ b/canaille/core/templates/partial/group-members.html @@ -0,0 +1,104 @@ +{% import "macro/table.html" as table %} + + + + {% if user.can_read("photo") %} + + {% endif %} + {% if user.can_read("user_name") %} + + {% endif %} + {% if user.can_read("family_name") or user.can_read("given_name") %} + + {% endif %} + {% if user.can_manage_groups %} + + + {% endif %} + + + + {% for watched_user in table_form.items_slice %} + + {% if user.can_read("photo") %} + + {% endif %} + {% if user.can_read("user_name") %} + + {% endif %} + {% if user.can_read("family_name") or user.can_read("given_name") %} + + {% endif %} + {% if user.can_manage_groups %} + + + {% endif %} + + {% else %} + + + + {% endfor %} + + + + + + +
{% trans %}Login{% endtrans %}{% trans %}Name{% endtrans %}{% trans %}Groups{% endtrans %}{% trans %}Actions{% endtrans %}
+ + {% if user.can_manage_users and watched_user.locked %} + + {% elif watched_user.photo and watched_user.photo %} + User photo + {% else %} + + {% endif %} + + + + {% if watched_user.user_name %} + {{ watched_user.user_name }} + {% else %} + {{ watched_user.identifier }} + {% endif %} + + {{ watched_user.formatted_name }} + {% for group in watched_user.groups %} + + {{ group.display_name }} + + {% endfor %} + + {% if edited_group.members|len > 1 %} +
+ {{ form.hidden_tag() if form.hidden_tag }} + + +
+ {% else %} + + {% endif %} +
+
+ +
+ {% if request.headers.get("Hx-Request") %} +
+ {% trans %}No item matches your request{% endtrans %} +
+

{% trans %}Maybe try with different criterias?{% endtrans %}

+ {% else %} +
+ {% trans %}There is nothing here{% endtrans %} +
+ {% endif %} +
+
+
+ {{ table.pagination(table_form) }} +
diff --git a/canaille/translations/messages.pot b/canaille/translations/messages.pot index 9167cea3..be3ac54f 100644 --- a/canaille/translations/messages.pot +++ b/canaille/translations/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-04-27 14:07+0200\n" +"POT-Creation-Date: 2024-04-28 19:05+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -53,8 +53,8 @@ msgstr "" msgid "John Doe" msgstr "" -#: canaille/backends/ldap/backend.py:176 canaille/core/endpoints/forms.py:149 -#: canaille/core/endpoints/forms.py:398 +#: canaille/backends/ldap/backend.py:176 canaille/core/endpoints/forms.py:164 +#: canaille/core/endpoints/forms.py:420 msgid "jdoe" msgstr "" @@ -128,9 +128,10 @@ msgstr "" msgid "You are already logged in, you cannot create an account." msgstr "" -#: canaille/core/endpoints/account.py:297 canaille/core/endpoints/forms.py:298 -#: canaille/core/endpoints/forms.py:416 canaille/core/templates/groups.html:5 +#: canaille/core/endpoints/account.py:297 canaille/core/endpoints/forms.py:313 +#: canaille/core/endpoints/forms.py:438 canaille/core/templates/groups.html:5 #: canaille/core/templates/groups.html:23 +#: canaille/core/templates/partial/group-members.html:15 #: canaille/core/templates/partial/users.html:18 #: canaille/templates/base.html:58 msgid "Groups" @@ -253,10 +254,10 @@ msgstr "" msgid "Email" msgstr "" -#: canaille/core/endpoints/admin.py:29 canaille/core/endpoints/forms.py:82 -#: canaille/core/endpoints/forms.py:105 canaille/core/endpoints/forms.py:194 -#: canaille/core/endpoints/forms.py:384 canaille/core/endpoints/forms.py:410 -#: canaille/core/endpoints/forms.py:431 canaille/core/endpoints/forms.py:447 +#: canaille/core/endpoints/admin.py:29 canaille/core/endpoints/forms.py:97 +#: canaille/core/endpoints/forms.py:120 canaille/core/endpoints/forms.py:209 +#: canaille/core/endpoints/forms.py:406 canaille/core/endpoints/forms.py:432 +#: canaille/core/endpoints/forms.py:453 canaille/core/endpoints/forms.py:469 msgid "jane@doe.com" msgstr "" @@ -325,57 +326,67 @@ msgstr "" msgid "Your password has been updated successfully" msgstr "" -#: canaille/core/endpoints/forms.py:30 +#: canaille/core/endpoints/forms.py:31 msgid "The user name '{user_name}' already exists" msgstr "" -#: canaille/core/endpoints/forms.py:39 +#: canaille/core/endpoints/forms.py:40 msgid "The email '{email}' is already used" msgstr "" -#: canaille/core/endpoints/forms.py:46 +#: canaille/core/endpoints/forms.py:47 msgid "The group '{group}' already exists" msgstr "" -#: canaille/core/endpoints/forms.py:55 +#: canaille/core/endpoints/forms.py:56 msgid "The login '{login}' does not exist" msgstr "" -#: canaille/core/endpoints/forms.py:71 +#: canaille/core/endpoints/forms.py:63 +msgid "The user you are trying to remove does not exist." +msgstr "" + +#: canaille/core/endpoints/forms.py:68 +msgid "The user '{user}' has already been removed from the group '{group}'" +msgstr "" + +#: canaille/core/endpoints/forms.py:86 msgid "" "The group '{group}' cannot be removed, because it must have at least one " "user left." msgstr "" -#: canaille/core/endpoints/forms.py:79 canaille/core/endpoints/forms.py:102 +#: canaille/core/endpoints/forms.py:94 canaille/core/endpoints/forms.py:117 +#: canaille/core/templates/partial/group-members.html:9 #: canaille/core/templates/partial/users.html:9 msgid "Login" msgstr "" -#: canaille/core/endpoints/forms.py:92 canaille/core/endpoints/forms.py:114 -#: canaille/core/endpoints/forms.py:248 +#: canaille/core/endpoints/forms.py:107 canaille/core/endpoints/forms.py:129 +#: canaille/core/endpoints/forms.py:263 #: canaille/core/templates/profile_settings.html:63 msgid "Password" msgstr "" -#: canaille/core/endpoints/forms.py:121 canaille/core/endpoints/forms.py:258 +#: canaille/core/endpoints/forms.py:136 canaille/core/endpoints/forms.py:273 msgid "Password confirmation" msgstr "" -#: canaille/core/endpoints/forms.py:124 canaille/core/endpoints/forms.py:261 +#: canaille/core/endpoints/forms.py:139 canaille/core/endpoints/forms.py:276 msgid "Password and confirmation do not match." msgstr "" -#: canaille/core/endpoints/forms.py:143 +#: canaille/core/endpoints/forms.py:158 msgid "Automatic" msgstr "" -#: canaille/core/endpoints/forms.py:148 +#: canaille/core/endpoints/forms.py:163 msgid "Username" msgstr "" -#: canaille/core/endpoints/forms.py:152 canaille/core/endpoints/forms.py:347 -#: canaille/core/endpoints/forms.py:361 +#: canaille/core/endpoints/forms.py:167 canaille/core/endpoints/forms.py:362 +#: canaille/core/endpoints/forms.py:376 +#: canaille/core/templates/partial/group-members.html:12 #: canaille/core/templates/partial/groups.html:6 #: canaille/core/templates/partial/users.html:12 #: canaille/oidc/endpoints/forms.py:26 @@ -383,187 +394,187 @@ msgstr "" msgid "Name" msgstr "" -#: canaille/core/endpoints/forms.py:154 +#: canaille/core/endpoints/forms.py:169 msgid "Title" msgstr "" -#: canaille/core/endpoints/forms.py:154 +#: canaille/core/endpoints/forms.py:169 msgid "Vice president" msgstr "" -#: canaille/core/endpoints/forms.py:157 +#: canaille/core/endpoints/forms.py:172 msgid "Given name" msgstr "" -#: canaille/core/endpoints/forms.py:159 +#: canaille/core/endpoints/forms.py:174 msgid "John" msgstr "" -#: canaille/core/endpoints/forms.py:165 +#: canaille/core/endpoints/forms.py:180 msgid "Family Name" msgstr "" -#: canaille/core/endpoints/forms.py:168 +#: canaille/core/endpoints/forms.py:183 msgid "Doe" msgstr "" -#: canaille/core/endpoints/forms.py:174 +#: canaille/core/endpoints/forms.py:189 msgid "Display Name" msgstr "" -#: canaille/core/endpoints/forms.py:177 +#: canaille/core/endpoints/forms.py:192 msgid "Johnny" msgstr "" -#: canaille/core/endpoints/forms.py:184 canaille/core/endpoints/forms.py:437 +#: canaille/core/endpoints/forms.py:199 canaille/core/endpoints/forms.py:459 #: canaille/core/templates/profile_edit.html:176 msgid "Email addresses" msgstr "" -#: canaille/core/endpoints/forms.py:190 canaille/core/endpoints/forms.py:427 +#: canaille/core/endpoints/forms.py:205 canaille/core/endpoints/forms.py:449 msgid "" "This email will be used as a recovery address to reset the password if " "needed" msgstr "" -#: canaille/core/endpoints/forms.py:204 +#: canaille/core/endpoints/forms.py:219 msgid "Phone numbers" msgstr "" -#: canaille/core/endpoints/forms.py:205 +#: canaille/core/endpoints/forms.py:220 msgid "555-000-555" msgstr "" -#: canaille/core/endpoints/forms.py:212 +#: canaille/core/endpoints/forms.py:227 msgid "Address" msgstr "" -#: canaille/core/endpoints/forms.py:214 +#: canaille/core/endpoints/forms.py:229 msgid "132, Foobar Street, Gotham City 12401, XX" msgstr "" -#: canaille/core/endpoints/forms.py:218 +#: canaille/core/endpoints/forms.py:233 msgid "Street" msgstr "" -#: canaille/core/endpoints/forms.py:220 +#: canaille/core/endpoints/forms.py:235 msgid "132, Foobar Street" msgstr "" -#: canaille/core/endpoints/forms.py:224 +#: canaille/core/endpoints/forms.py:239 msgid "Postal Code" msgstr "" -#: canaille/core/endpoints/forms.py:230 +#: canaille/core/endpoints/forms.py:245 msgid "Locality" msgstr "" -#: canaille/core/endpoints/forms.py:232 +#: canaille/core/endpoints/forms.py:247 msgid "Gotham City" msgstr "" -#: canaille/core/endpoints/forms.py:236 +#: canaille/core/endpoints/forms.py:251 msgid "Region" msgstr "" -#: canaille/core/endpoints/forms.py:238 +#: canaille/core/endpoints/forms.py:253 msgid "North Pole" msgstr "" -#: canaille/core/endpoints/forms.py:242 +#: canaille/core/endpoints/forms.py:257 msgid "Photo" msgstr "" -#: canaille/core/endpoints/forms.py:246 +#: canaille/core/endpoints/forms.py:261 #: canaille/core/templates/profile_add.html:56 #: canaille/core/templates/profile_edit.html:64 msgid "Delete the photo" msgstr "" -#: canaille/core/endpoints/forms.py:269 +#: canaille/core/endpoints/forms.py:284 msgid "User number" msgstr "" -#: canaille/core/endpoints/forms.py:271 canaille/core/endpoints/forms.py:277 +#: canaille/core/endpoints/forms.py:286 canaille/core/endpoints/forms.py:292 msgid "1234" msgstr "" -#: canaille/core/endpoints/forms.py:275 +#: canaille/core/endpoints/forms.py:290 msgid "Department" msgstr "" -#: canaille/core/endpoints/forms.py:281 +#: canaille/core/endpoints/forms.py:296 msgid "Organization" msgstr "" -#: canaille/core/endpoints/forms.py:283 +#: canaille/core/endpoints/forms.py:298 msgid "Cogip LTD." msgstr "" -#: canaille/core/endpoints/forms.py:287 +#: canaille/core/endpoints/forms.py:302 msgid "Website" msgstr "" -#: canaille/core/endpoints/forms.py:289 +#: canaille/core/endpoints/forms.py:304 msgid "https://mywebsite.tld" msgstr "" -#: canaille/core/endpoints/forms.py:294 +#: canaille/core/endpoints/forms.py:309 msgid "Preferred language" msgstr "" -#: canaille/core/endpoints/forms.py:301 +#: canaille/core/endpoints/forms.py:316 msgid "users, admins …" msgstr "" -#: canaille/core/endpoints/forms.py:326 +#: canaille/core/endpoints/forms.py:341 msgid "Account expiration" msgstr "" -#: canaille/core/endpoints/forms.py:350 +#: canaille/core/endpoints/forms.py:365 msgid "group" msgstr "" -#: canaille/core/endpoints/forms.py:354 canaille/core/endpoints/forms.py:371 +#: canaille/core/endpoints/forms.py:369 canaille/core/endpoints/forms.py:386 #: canaille/core/templates/partial/groups.html:7 msgid "Description" msgstr "" -#: canaille/core/endpoints/forms.py:378 canaille/core/endpoints/forms.py:403 +#: canaille/core/endpoints/forms.py:400 canaille/core/endpoints/forms.py:425 msgid "Email address" msgstr "" -#: canaille/core/endpoints/forms.py:397 +#: canaille/core/endpoints/forms.py:419 msgid "User name" msgstr "" -#: canaille/core/endpoints/forms.py:401 +#: canaille/core/endpoints/forms.py:423 msgid "Username editable by the invitee" msgstr "" -#: canaille/core/endpoints/forms.py:440 +#: canaille/core/endpoints/forms.py:462 msgid "New email address" msgstr "" -#: canaille/core/endpoints/groups.py:38 +#: canaille/core/endpoints/groups.py:39 msgid "Group creation failed." msgstr "" -#: canaille/core/endpoints/groups.py:46 +#: canaille/core/endpoints/groups.py:47 #, python-format msgid "The group %(group)s has been sucessfully created" msgstr "" -#: canaille/core/endpoints/groups.py:96 +#: canaille/core/endpoints/groups.py:107 #, python-format msgid "The group %(group)s has been sucessfully edited." msgstr "" -#: canaille/core/endpoints/groups.py:104 +#: canaille/core/endpoints/groups.py:115 msgid "Group edition failed." msgstr "" -#: canaille/core/endpoints/groups.py:118 +#: canaille/core/endpoints/groups.py:159 #, python-format msgid "The group %(group)s has been sucessfully deleted" msgstr "" @@ -1262,6 +1273,7 @@ msgstr "" #: canaille/core/templates/modals/delete-account.html:26 #: canaille/core/templates/modals/delete-group.html:22 #: canaille/core/templates/modals/lock-account.html:26 +#: canaille/core/templates/modals/remove-group-member.html:22 #: canaille/oidc/templates/modals/delete-client.html:20 #: canaille/oidc/templates/modals/revoke-token.html:20 msgid "Cancel" @@ -1305,10 +1317,32 @@ msgstr "" msgid "Lock" msgstr "" -#: canaille/core/templates/partial/groups.html:8 -msgid "Number of members" +#: canaille/core/templates/modals/remove-group-member.html:10 +msgid "Group member deletion" msgstr "" +#: canaille/core/templates/modals/remove-group-member.html:14 +#, python-format +msgid "" +"Are you sure you want to remove %(user_name)s from the group " +"\"%(group_name)s\"?" +msgstr "" + +#: canaille/core/templates/modals/remove-group-member.html:25 +#: canaille/core/templates/partial/group-members.html:64 +msgid "Remove" +msgstr "" + +#: canaille/core/templates/partial/group-members.html:16 +msgid "Actions" +msgstr "" + +#: canaille/core/templates/partial/group-members.html:27 +#: canaille/core/templates/partial/users.html:29 +msgid "This account is locked" +msgstr "" + +#: canaille/core/templates/partial/group-members.html:79 #: canaille/core/templates/partial/groups.html:31 #: canaille/core/templates/partial/users.html:73 #: canaille/oidc/templates/partial/authorization_list.html:31 @@ -1317,6 +1351,7 @@ msgstr "" msgid "No item matches your request" msgstr "" +#: canaille/core/templates/partial/group-members.html:81 #: canaille/core/templates/partial/groups.html:33 #: canaille/core/templates/partial/users.html:75 #: canaille/oidc/templates/partial/authorization_list.html:33 @@ -1325,6 +1360,7 @@ msgstr "" msgid "Maybe try with different criterias?" msgstr "" +#: canaille/core/templates/partial/group-members.html:84 #: canaille/core/templates/partial/groups.html:36 #: canaille/core/templates/partial/users.html:78 #: canaille/oidc/templates/partial/authorization_list.html:36 @@ -1334,8 +1370,8 @@ msgstr "" msgid "There is nothing here" msgstr "" -#: canaille/core/templates/partial/users.html:29 -msgid "This account is locked" +#: canaille/core/templates/partial/groups.html:8 +msgid "Number of members" msgstr "" #: canaille/oidc/utils.py:6 diff --git a/tests/core/test_groups.py b/tests/core/test_groups.py index 48ecf611..c8f24744 100644 --- a/tests/core/test_groups.py +++ b/tests/core/test_groups.py @@ -292,3 +292,95 @@ def test_user_list_search(testclient, logged_admin, foo_group, user, moderator): res.mustcontain(user.formatted_name) res.mustcontain(no=logged_admin.formatted_name) res.mustcontain(no=moderator.formatted_name) + + +def test_remove_member(testclient, logged_admin, foo_group, user, moderator): + foo_group.members = [user, moderator] + foo_group.save() + + res = testclient.get("/groups/foo") + form = res.forms[f"deletegroupmemberform-{user.id}"] + + res = form.submit(name="action", value="confirm-remove-member") + res = res.form.submit(name="action", value="remove-member") + assert ( + "success", + "John (johnny) Doe has been removed from the group foo", + ) in res.flashes + + foo_group.reload() + assert user not in foo_group.members + + +def test_remove_member_already_remove_from_group( + testclient, logged_admin, foo_group, user, moderator +): + foo_group.members = [user, moderator] + foo_group.save() + + res = testclient.get("/groups/foo") + form = res.forms[f"deletegroupmemberform-{user.id}"] + foo_group.members = [moderator] + foo_group.save() + + res = form.submit(name="action", value="confirm-remove-member") + assert ( + "error", + "The user 'John (johnny) Doe' has already been removed from the group 'foo'", + ) in res.flashes + + +def test_confirm_remove_member_already_removed_from_group( + testclient, logged_admin, foo_group, user, moderator +): + foo_group.members = [user, moderator] + foo_group.save() + + res = testclient.get("/groups/foo") + form = res.forms[f"deletegroupmemberform-{user.id}"] + res = form.submit(name="action", value="confirm-remove-member") + + foo_group.members = [moderator] + foo_group.save() + res = res.form.submit(name="action", value="remove-member") + + assert ( + "error", + "The user 'John (johnny) Doe' has already been removed from the group 'foo'", + ) in res.flashes + + +def test_remove_member_already_deleted( + testclient, logged_admin, foo_group, user, moderator +): + foo_group.members = [user, moderator] + foo_group.save() + + res = testclient.get("/groups/foo") + form = res.forms[f"deletegroupmemberform-{user.id}"] + user.delete() + + res = form.submit(name="action", value="confirm-remove-member") + assert ( + "error", + "The user you are trying to remove does not exist.", + ) in res.flashes + + +def test_confirm_remove_member_already_deleted( + testclient, logged_admin, foo_group, user, moderator +): + foo_group.members = [user, moderator] + foo_group.save() + + res = testclient.get("/groups/foo") + form = res.forms[f"deletegroupmemberform-{user.id}"] + res = form.submit(name="action", value="confirm-remove-member") + + user.delete() + res = res.form.submit(name="action", value="remove-member") + + assert ( + "error", + "The user you are trying to remove does not exist.", + ) in res.flashes