From 19136f6f05978d03a88128835d247bd71c66d0eb Mon Sep 17 00:00:00 2001 From: Rouven Seifert Date: Thu, 26 Sep 2024 19:14:55 +0200 Subject: [PATCH] allow memberships beginning in the future to be deleted --- pycroft/lib/membership.py | 15 +++++++ web/blueprints/user/__init__.py | 79 ++++++++++++++++++++++++++++++++- web/blueprints/user/tables.py | 1 + 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/pycroft/lib/membership.py b/pycroft/lib/membership.py index b8f3795e8..13ed85bcf 100644 --- a/pycroft/lib/membership.py +++ b/pycroft/lib/membership.py @@ -162,6 +162,21 @@ def remove_member_of( user=user, author=processor) +def delete_membership( + session: Session, + membership_id: int, + processor: User, +) -> None: + membership = session.get(Membership, membership_id) + session.delete(membership) + message = deferred_gettext("Deleted membership of group {group}.") + log_user_event( + message.format(group=membership.group.name).to_json(), + user=membership.user, + author=processor, + ) + + @with_transaction def edit_property_group( group: PropertyGroup, name: str, permission_level: int, processor: User diff --git a/web/blueprints/user/__init__.py b/web/blueprints/user/__init__.py index 60f78e31e..efa7dcd33 100644 --- a/web/blueprints/user/__init__.py +++ b/web/blueprints/user/__init__.py @@ -32,6 +32,7 @@ ) from flask.typing import ResponseReturnValue from flask_login import current_user +from flask_wtf import FlaskForm from markupsafe import Markup import pycroft.lib.search @@ -44,7 +45,12 @@ from pycroft.helpers.net import ip_regex, mac_regex from pycroft.lib.facilities import get_room from pycroft.lib.logging import log_user_event -from pycroft.lib.membership import make_member_of, remove_member_of, change_membership_active_during +from pycroft.lib.membership import ( + make_member_of, + remove_member_of, + change_membership_active_during, + delete_membership, +) from pycroft.lib.traffic import get_users_with_highest_traffic from pycroft.lib.user import encode_type1_user_id, encode_type2_user_id, \ traffic_history, generate_user_sheet, get_blocked_groups, \ @@ -454,6 +460,13 @@ def user_show_groups_json( membership_id=membership.id, ) ), + url_delete=( + url_delete := url_for( + ".membership_delete", + user_id=user_id, + membership_id=membership.id, + ) + ), url_end=( url_end := ( url_for( @@ -484,6 +497,18 @@ def user_show_groups_json( ] if active else [] + ) + + ( + [ + BtnColResponse( + href=url_delete, + title="Löschen", + icon="fa-trash", + btn_class="btn-link", + ) + ] + if session.utcnow() <= membership.active_during.begin + else [] ), ) for membership, granted, denied in memberships @@ -748,6 +773,58 @@ def default_response(refill_form_data: bool = False) -> ResponseReturnValue: return redirect(url_for(".user_show", user_id=user.id)) +@bp.route("//delete_membership//", methods=["GET", "POST"]) +@access.require("groups_change_membership") +def membership_delete(user_id: int, membership_id: int) -> ResponseReturnValue: + membership = get_membership_or_404(membership_id) + assert isinstance(membership.group, PropertyGroup) + if membership.group.permission_level > current_user.permission_level: + flash( + "Eine Bearbeitung von Gruppenmitgliedschaften für Gruppen mit " + "höherem Berechtigungslevel ist nicht möglich.", + "error", + ) + abort(403) + if session.utcnow() >= membership.active_during.begin: + flash("Nur Mitgliedschaften, die in der Zukunft liegen, können gelöscht werden.", "error") + abort(403) + form = FlaskForm() + + def default_response() -> ResponseReturnValue: + form_args = { + "form": form, + "cancel_to": url_for("user.user_show", user_id=membership.user_id, _anchor="groups"), + "submit_text": "Löschen", + "actions_offset": 0, + } + + return render_template( + "generic_form.html", + page_title=( + "Mitgliedschaft {} für " + "{} löschen".format(membership.group.name, membership.user.name) + ), + membership_id=membership_id, + user=membership.user, + form=form, + form_args=form_args, + ) + + if not form.is_submitted(): + return default_response() + + with abort_on_error(default_response), session.session.begin_nested(): + delete_membership( + session=session.session, + membership_id=membership_id, + processor=current_user, + ) + session.session.commit() + + flash("Mitgliedschaft erfolgreich gelöscht.", "success") + return redirect(url_for("user.user_show", user_id=membership.user_id, _anchor="groups")) + + @bp.route('//edit_membership/', methods=['GET', 'POST']) @access.require('groups_change_membership') def edit_membership(user_id: int, membership_id: int) -> ResponseReturnValue: diff --git a/web/blueprints/user/tables.py b/web/blueprints/user/tables.py index 2e345163e..61f7b417d 100644 --- a/web/blueprints/user/tables.py +++ b/web/blueprints/user/tables.py @@ -60,6 +60,7 @@ class MembershipRow(BaseModel): ends_at: DateColResponse url_edit: str url_end: str | None = None + url_delete: str | None = None actions: list[BtnColResponse] # used by membershipRowAttributes grants: list[str | None]