From 545d2339e843bd75cec5c93c642f65b618894aba Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Wed, 3 May 2023 22:51:55 +0200 Subject: [PATCH] Leave organization #458 --- docker-compose.test.services.yml | 25 ++++++ messages.pot | 48 ++++++---- project/access.py | 16 ++++ project/forms/admin_unit.py | 5 ++ project/templates/layout.html | 1 + .../templates/manage/delete_membership.html | 24 +++++ .../translations/de/LC_MESSAGES/messages.mo | Bin 39680 -> 40074 bytes .../translations/de/LC_MESSAGES/messages.po | 52 +++++++---- .../translations/en/LC_MESSAGES/messages.mo | Bin 4079 -> 4079 bytes .../translations/en/LC_MESSAGES/messages.po | 48 ++++++---- project/views/admin_unit_member.py | 5 +- project/views/manage.py | 56 +++++++++++- tests/seeder.py | 17 +++- tests/utils.py | 4 +- tests/views/test_admin_unit_member.py | 33 +++++-- tests/views/test_manage.py | 85 ++++++++++++++++++ 16 files changed, 359 insertions(+), 60 deletions(-) create mode 100644 docker-compose.test.services.yml create mode 100644 project/templates/manage/delete_membership.html diff --git a/docker-compose.test.services.yml b/docker-compose.test.services.yml new file mode 100644 index 0000000..f60dd78 --- /dev/null +++ b/docker-compose.test.services.yml @@ -0,0 +1,25 @@ +version: "3.9" +name: "eventcally-test-services" + +services: + db: + image: postgis/postgis:12-3.1 + healthcheck: + test: "pg_isready --username=eventcally && psql --username=eventcally --list" + start_period: "5s" + ports: + - 5433:5432 + environment: + - POSTGRES_DB=eventcally + - POSTGRES_USER=eventcally + - POSTGRES_PASSWORD=pass + + redis: + image: bitnami/redis:6.2 + healthcheck: + test: "redis-cli -a 'pass' ping | grep PONG" + start_period: "5s" + ports: + - 6380:6379 + environment: + REDIS_PASSWORD: pass diff --git a/messages.pot b/messages.pot index 83c3c68..317e3a1 100644 --- a/messages.pot +++ b/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2023-05-02 23:21+0200\n" +"POT-Creation-Date: 2023-05-03 20:26+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -198,24 +198,24 @@ msgstr "" msgid "You have received an invitation" msgstr "" -#: project/forms/admin.py:11 project/templates/layout.html:303 +#: project/forms/admin.py:11 project/templates/layout.html:304 #: project/views/root.py:55 msgid "Terms of service" msgstr "" -#: project/forms/admin.py:12 project/templates/layout.html:307 +#: project/forms/admin.py:12 project/templates/layout.html:308 #: project/views/root.py:63 msgid "Legal notice" msgstr "" #: project/forms/admin.py:13 project/templates/_macros.html:1473 -#: project/templates/layout.html:311 +#: project/templates/layout.html:312 #: project/templates/widget/event_suggestion/create.html:204 #: project/views/admin_unit.py:83 project/views/root.py:71 msgid "Contact" msgstr "" -#: project/forms/admin.py:14 project/templates/layout.html:315 +#: project/forms/admin.py:14 project/templates/layout.html:316 #: project/views/root.py:79 msgid "Privacy" msgstr "" @@ -310,11 +310,11 @@ msgstr "" #: project/forms/admin.py:69 project/forms/admin_unit.py:28 #: project/forms/admin_unit.py:134 project/forms/admin_unit.py:139 -#: project/forms/event.py:85 project/forms/event.py:114 -#: project/forms/event_place.py:25 project/forms/event_place.py:50 -#: project/forms/event_suggestion.py:26 project/forms/oauth2_client.py:66 -#: project/forms/organizer.py:25 project/forms/organizer.py:52 -#: project/forms/reference_request.py:23 +#: project/forms/admin_unit.py:144 project/forms/event.py:85 +#: project/forms/event.py:114 project/forms/event_place.py:25 +#: project/forms/event_place.py:50 project/forms/event_suggestion.py:26 +#: project/forms/oauth2_client.py:66 project/forms/organizer.py:25 +#: project/forms/organizer.py:52 project/forms/reference_request.py:23 #: project/templates/admin/admin_units.html:19 #: project/templates/event_place/list.html:19 #: project/templates/manage/organizers.html:18 @@ -487,6 +487,11 @@ msgstr "" msgid "Cancel deletion" msgstr "" +#: project/forms/admin_unit.py:143 project/templates/layout.html:276 +#: project/templates/manage/delete_membership.html:6 +msgid "Leave organization" +msgstr "" + #: project/forms/admin_unit_member.py:13 msgid "Invite" msgstr "" @@ -1142,6 +1147,7 @@ msgstr "" #: project/templates/admin_unit/request_deletion.html:15 #: project/templates/admin_unit/update.html:36 #: project/templates/layout.html:247 +#: project/templates/manage/delete_membership.html:13 msgid "Organization" msgstr "" @@ -1668,7 +1674,7 @@ msgid "Switch organization" msgstr "" #: project/templates/developer/read.html:4 -#: project/templates/developer/read.html:8 project/templates/layout.html:319 +#: project/templates/developer/read.html:8 project/templates/layout.html:320 #: project/templates/profile.html:46 msgid "Developer" msgstr "" @@ -2262,7 +2268,7 @@ msgid "Organization successfully updated" msgstr "" #: project/views/admin.py:85 project/views/admin_unit.py:187 -#: project/views/admin_unit.py:220 +#: project/views/admin_unit.py:220 project/views/manage.py:316 msgid "Entered name does not match organization name" msgstr "" @@ -2270,7 +2276,7 @@ msgstr "" msgid "Organization successfully deleted" msgstr "" -#: project/views/admin.py:113 project/views/manage.py:432 +#: project/views/admin.py:113 project/views/manage.py:486 #: project/views/user.py:41 msgid "Settings successfully updated" msgstr "" @@ -2323,11 +2329,15 @@ msgstr "" msgid "Member successfully updated" msgstr "" -#: project/views/admin_unit_member.py:69 +#: project/views/admin_unit_member.py:70 project/views/manage.py:307 +msgid "Last remaining administrator can not leave the organization." +msgstr "" + +#: project/views/admin_unit_member.py:79 msgid "Entered email does not match member email" msgstr "" -#: project/views/admin_unit_member.py:74 +#: project/views/admin_unit_member.py:84 msgid "Member successfully deleted" msgstr "" @@ -2420,6 +2430,14 @@ msgstr "" msgid "Places of Google Maps" msgstr "" +#: project/views/manage.py:302 +msgid "You are not a member of this organization" +msgstr "" + +#: project/views/manage.py:321 +msgid "Organization successfully left" +msgstr "" + #: project/views/oauth2_client.py:37 msgid "OAuth2 client successfully created" msgstr "" diff --git a/project/access.py b/project/access.py index ff6b14e..2bb44b7 100644 --- a/project/access.py +++ b/project/access.py @@ -8,6 +8,7 @@ from sqlalchemy import and_ from project import app from project.models import AdminUnit, AdminUnitMember, Event, PublicStatus, User +from project.models.admin_unit import AdminUnitMemberRole from project.services.admin_unit import get_member_for_admin_unit_by_user_id @@ -238,3 +239,18 @@ def get_admin_unit_members_with_permission(admin_unit_id: int, permission: str) lambda member: has_admin_unit_member_permission(member, permission), members ) ) + + +def can_current_user_delete_member(member: AdminUnitMember) -> bool: + if current_user.has_role("admin"): + return True + + # Check if there is another admin + return ( + AdminUnitMember.query.filter( + AdminUnitMember.user_id != member.user_id, + AdminUnitMember.admin_unit_id == member.admin_unit_id, + AdminUnitMember.roles.any(AdminUnitMemberRole.name == "admin"), + ).first() + is not None + ) diff --git a/project/forms/admin_unit.py b/project/forms/admin_unit.py index 4e2af7e..c689d3f 100644 --- a/project/forms/admin_unit.py +++ b/project/forms/admin_unit.py @@ -137,3 +137,8 @@ class RequestAdminUnitDeletionForm(FlaskForm): class CancelAdminUnitDeletionForm(FlaskForm): submit = SubmitField(lazy_gettext("Cancel deletion")) name = StringField(lazy_gettext("Name"), validators=[DataRequired()]) + + +class AdminUnitDeleteMembershipForm(FlaskForm): + submit = SubmitField(lazy_gettext("Leave organization")) + name = StringField(lazy_gettext("Name"), validators=[DataRequired()]) diff --git a/project/templates/layout.html b/project/templates/layout.html index d1b3af4..8bd9698 100644 --- a/project/templates/layout.html +++ b/project/templates/layout.html @@ -273,6 +273,7 @@ diff --git a/project/templates/manage/delete_membership.html b/project/templates/manage/delete_membership.html new file mode 100644 index 0000000..da85975 --- /dev/null +++ b/project/templates/manage/delete_membership.html @@ -0,0 +1,24 @@ +{% extends "layout.html" %} +{% from "_macros.html" import render_field_with_errors, render_field %} + +{% block content %} + +

{{ _('Leave organization') }} "{{ admin_unit.name }}"

+ +
+ {{ form.hidden_tag() }} + +
+
+ {{ _('Organization') }} +
+
+ {{ render_field_with_errors(form.name) }} +
+
+ + {{ render_field(form.submit) }} + +
+ +{% endblock %} diff --git a/project/translations/de/LC_MESSAGES/messages.mo b/project/translations/de/LC_MESSAGES/messages.mo index aff23f37379e439e495d7185951701dfb78d0b1e..58114db2d37641a1c49cfcad3e0697c7af01ba45 100644 GIT binary patch delta 9047 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 delta 8748 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\n" "Language: de\n" @@ -199,24 +199,24 @@ msgstr "message" msgid "You have received an invitation" msgstr "Du hast eine Einladung erhalten" -#: project/forms/admin.py:11 project/templates/layout.html:303 +#: project/forms/admin.py:11 project/templates/layout.html:304 #: project/views/root.py:55 msgid "Terms of service" msgstr "Nutzungsbedingungen" -#: project/forms/admin.py:12 project/templates/layout.html:307 +#: project/forms/admin.py:12 project/templates/layout.html:308 #: project/views/root.py:63 msgid "Legal notice" msgstr "Impressum" #: project/forms/admin.py:13 project/templates/_macros.html:1473 -#: project/templates/layout.html:311 +#: project/templates/layout.html:312 #: project/templates/widget/event_suggestion/create.html:204 #: project/views/admin_unit.py:83 project/views/root.py:71 msgid "Contact" msgstr "Kontakt" -#: project/forms/admin.py:14 project/templates/layout.html:315 +#: project/forms/admin.py:14 project/templates/layout.html:316 #: project/views/root.py:79 msgid "Privacy" msgstr "Datenschutz" @@ -319,11 +319,11 @@ msgstr "Organisation löschen" #: project/forms/admin.py:69 project/forms/admin_unit.py:28 #: project/forms/admin_unit.py:134 project/forms/admin_unit.py:139 -#: project/forms/event.py:85 project/forms/event.py:114 -#: project/forms/event_place.py:25 project/forms/event_place.py:50 -#: project/forms/event_suggestion.py:26 project/forms/oauth2_client.py:66 -#: project/forms/organizer.py:25 project/forms/organizer.py:52 -#: project/forms/reference_request.py:23 +#: project/forms/admin_unit.py:144 project/forms/event.py:85 +#: project/forms/event.py:114 project/forms/event_place.py:25 +#: project/forms/event_place.py:50 project/forms/event_suggestion.py:26 +#: project/forms/oauth2_client.py:66 project/forms/organizer.py:25 +#: project/forms/organizer.py:52 project/forms/reference_request.py:23 #: project/templates/admin/admin_units.html:19 #: project/templates/event_place/list.html:19 #: project/templates/manage/organizers.html:18 @@ -507,6 +507,11 @@ msgstr "Löschung beantragen" msgid "Cancel deletion" msgstr "Löschen abbrechen" +#: project/forms/admin_unit.py:143 project/templates/layout.html:276 +#: project/templates/manage/delete_membership.html:6 +msgid "Leave organization" +msgstr "Organisation verlassen" + #: project/forms/admin_unit_member.py:13 msgid "Invite" msgstr "Einladen" @@ -1190,6 +1195,7 @@ msgstr "Wochentage" #: project/templates/admin_unit/request_deletion.html:15 #: project/templates/admin_unit/update.html:36 #: project/templates/layout.html:247 +#: project/templates/manage/delete_membership.html:13 msgid "Organization" msgstr "Organisation" @@ -1725,7 +1731,7 @@ msgid "Switch organization" msgstr "Organisation wechseln" #: project/templates/developer/read.html:4 -#: project/templates/developer/read.html:8 project/templates/layout.html:319 +#: project/templates/developer/read.html:8 project/templates/layout.html:320 #: project/templates/profile.html:46 msgid "Developer" msgstr "Entwickler" @@ -2334,7 +2340,7 @@ msgid "Organization successfully updated" msgstr "Organisation erfolgreich aktualisiert" #: project/views/admin.py:85 project/views/admin_unit.py:187 -#: project/views/admin_unit.py:220 +#: project/views/admin_unit.py:220 project/views/manage.py:316 msgid "Entered name does not match organization name" msgstr "Der eingegebene Name entspricht nicht dem Namen der Organisation" @@ -2342,7 +2348,7 @@ msgstr "Der eingegebene Name entspricht nicht dem Namen der Organisation" msgid "Organization successfully deleted" msgstr "Organisation erfolgreich gelöscht" -#: project/views/admin.py:113 project/views/manage.py:432 +#: project/views/admin.py:113 project/views/manage.py:486 #: project/views/user.py:41 msgid "Settings successfully updated" msgstr "Einstellungen erfolgreich aktualisiert" @@ -2398,11 +2404,15 @@ msgstr "Löschung der Organisation beantragt" msgid "Member successfully updated" msgstr "Mitglied erfolgreich aktualisiert" -#: project/views/admin_unit_member.py:69 +#: project/views/admin_unit_member.py:70 project/views/manage.py:307 +msgid "Last remaining administrator can not leave the organization." +msgstr "Der letzte verbleibende Administrator kann die Organisation nicht verlassen." + +#: project/views/admin_unit_member.py:79 msgid "Entered email does not match member email" msgstr "Die eingegebene Email passt nicht zur Email des Mitglieds" -#: project/views/admin_unit_member.py:74 +#: project/views/admin_unit_member.py:84 msgid "Member successfully deleted" msgstr "Mitglied erfolgreich gelöscht" @@ -2495,6 +2505,14 @@ msgstr "Orte der Organisation" msgid "Places of Google Maps" msgstr "Orte von Google Maps" +#: project/views/manage.py:302 +msgid "You are not a member of this organization" +msgstr "Du bist kein Mitglied dieser Organisation" + +#: project/views/manage.py:321 +msgid "Organization successfully left" +msgstr "Organisation erfolgreich verlassen" + #: project/views/oauth2_client.py:37 msgid "OAuth2 client successfully created" msgstr "OAuth2 Client erfolgreich erstellt" @@ -2585,7 +2603,9 @@ msgstr "" msgid "" "You are administrator of at least one organization. Cancel your " "membership to delete your account." -msgstr "Du bist Administrator von mindestens einer Organisation. Beende deine Mitgliedschaft, um deinen Account zu löschen." +msgstr "" +"Du bist Administrator von mindestens einer Organisation. Beende deine " +"Mitgliedschaft, um deinen Account zu löschen." #: project/views/user.py:92 project/views/user.py:119 msgid "Entered email does not match your email" diff --git a/project/translations/en/LC_MESSAGES/messages.mo b/project/translations/en/LC_MESSAGES/messages.mo index 11a700d95c2788f18db26b6d17cfa548c7325c38..c9b53b0758a1c399482e4504ef24f85062fe4e82 100644 GIT binary patch delta 20 ccmaDa|6YE>Yfg4!1tSA1BeTt)IRCN&09L~X&j0`b delta 20 ccmaDa|6YE>Yfg3}1tViCBg4&~IRCN&09LdI%m4rY diff --git a/project/translations/en/LC_MESSAGES/messages.po b/project/translations/en/LC_MESSAGES/messages.po index cf72353..6297aae 100644 --- a/project/translations/en/LC_MESSAGES/messages.po +++ b/project/translations/en/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2023-05-02 23:21+0200\n" +"POT-Creation-Date: 2023-05-03 20:26+0200\n" "PO-Revision-Date: 2021-04-30 15:04+0200\n" "Last-Translator: FULL NAME \n" "Language: en\n" @@ -199,24 +199,24 @@ msgstr "" msgid "You have received an invitation" msgstr "" -#: project/forms/admin.py:11 project/templates/layout.html:303 +#: project/forms/admin.py:11 project/templates/layout.html:304 #: project/views/root.py:55 msgid "Terms of service" msgstr "" -#: project/forms/admin.py:12 project/templates/layout.html:307 +#: project/forms/admin.py:12 project/templates/layout.html:308 #: project/views/root.py:63 msgid "Legal notice" msgstr "" #: project/forms/admin.py:13 project/templates/_macros.html:1473 -#: project/templates/layout.html:311 +#: project/templates/layout.html:312 #: project/templates/widget/event_suggestion/create.html:204 #: project/views/admin_unit.py:83 project/views/root.py:71 msgid "Contact" msgstr "" -#: project/forms/admin.py:14 project/templates/layout.html:315 +#: project/forms/admin.py:14 project/templates/layout.html:316 #: project/views/root.py:79 msgid "Privacy" msgstr "" @@ -311,11 +311,11 @@ msgstr "" #: project/forms/admin.py:69 project/forms/admin_unit.py:28 #: project/forms/admin_unit.py:134 project/forms/admin_unit.py:139 -#: project/forms/event.py:85 project/forms/event.py:114 -#: project/forms/event_place.py:25 project/forms/event_place.py:50 -#: project/forms/event_suggestion.py:26 project/forms/oauth2_client.py:66 -#: project/forms/organizer.py:25 project/forms/organizer.py:52 -#: project/forms/reference_request.py:23 +#: project/forms/admin_unit.py:144 project/forms/event.py:85 +#: project/forms/event.py:114 project/forms/event_place.py:25 +#: project/forms/event_place.py:50 project/forms/event_suggestion.py:26 +#: project/forms/oauth2_client.py:66 project/forms/organizer.py:25 +#: project/forms/organizer.py:52 project/forms/reference_request.py:23 #: project/templates/admin/admin_units.html:19 #: project/templates/event_place/list.html:19 #: project/templates/manage/organizers.html:18 @@ -488,6 +488,11 @@ msgstr "" msgid "Cancel deletion" msgstr "" +#: project/forms/admin_unit.py:143 project/templates/layout.html:276 +#: project/templates/manage/delete_membership.html:6 +msgid "Leave organization" +msgstr "" + #: project/forms/admin_unit_member.py:13 msgid "Invite" msgstr "" @@ -1143,6 +1148,7 @@ msgstr "" #: project/templates/admin_unit/request_deletion.html:15 #: project/templates/admin_unit/update.html:36 #: project/templates/layout.html:247 +#: project/templates/manage/delete_membership.html:13 msgid "Organization" msgstr "" @@ -1676,7 +1682,7 @@ msgid "Switch organization" msgstr "" #: project/templates/developer/read.html:4 -#: project/templates/developer/read.html:8 project/templates/layout.html:319 +#: project/templates/developer/read.html:8 project/templates/layout.html:320 #: project/templates/profile.html:46 msgid "Developer" msgstr "" @@ -2270,7 +2276,7 @@ msgid "Organization successfully updated" msgstr "" #: project/views/admin.py:85 project/views/admin_unit.py:187 -#: project/views/admin_unit.py:220 +#: project/views/admin_unit.py:220 project/views/manage.py:316 msgid "Entered name does not match organization name" msgstr "" @@ -2278,7 +2284,7 @@ msgstr "" msgid "Organization successfully deleted" msgstr "" -#: project/views/admin.py:113 project/views/manage.py:432 +#: project/views/admin.py:113 project/views/manage.py:486 #: project/views/user.py:41 msgid "Settings successfully updated" msgstr "" @@ -2331,11 +2337,15 @@ msgstr "" msgid "Member successfully updated" msgstr "" -#: project/views/admin_unit_member.py:69 +#: project/views/admin_unit_member.py:70 project/views/manage.py:307 +msgid "Last remaining administrator can not leave the organization." +msgstr "" + +#: project/views/admin_unit_member.py:79 msgid "Entered email does not match member email" msgstr "" -#: project/views/admin_unit_member.py:74 +#: project/views/admin_unit_member.py:84 msgid "Member successfully deleted" msgstr "" @@ -2428,6 +2438,14 @@ msgstr "" msgid "Places of Google Maps" msgstr "" +#: project/views/manage.py:302 +msgid "You are not a member of this organization" +msgstr "" + +#: project/views/manage.py:321 +msgid "Organization successfully left" +msgstr "" + #: project/views/oauth2_client.py:37 msgid "OAuth2 client successfully created" msgstr "" diff --git a/project/views/admin_unit_member.py b/project/views/admin_unit_member.py index 84b50d2..71317cc 100644 --- a/project/views/admin_unit_member.py +++ b/project/views/admin_unit_member.py @@ -1,6 +1,6 @@ from flask import flash, redirect, render_template, url_for from flask_babel import gettext -from flask_security import auth_required +from flask_security import auth_required, current_user from sqlalchemy.exc import SQLAlchemyError from project import app, db @@ -59,6 +59,9 @@ def manage_admin_unit_member_delete(id): member = AdminUnitMember.query.get_or_404(id) admin_unit = member.adminunit + if member.user_id == current_user.id: + return redirect(url_for("manage_admin_unit_delete_membership", id=id)) + if not has_access(admin_unit, "admin_unit.members:delete"): return permission_missing(url_for("manage_admin_unit", id=admin_unit.id)) diff --git a/project/views/manage.py b/project/views/manage.py index e1590e7..b909006 100644 --- a/project/views/manage.py +++ b/project/views/manage.py @@ -17,12 +17,16 @@ from project import app, db, dump_org_path from project.access import ( access_or_401, admin_unit_suggestions_enabled_or_404, + can_current_user_delete_member, get_admin_unit_for_manage_or_404, get_admin_units_for_manage, has_access, ) from project.celery_tasks import dump_admin_unit_task -from project.forms.admin_unit import UpdateAdminUnitWidgetForm +from project.forms.admin_unit import ( + AdminUnitDeleteMembershipForm, + UpdateAdminUnitWidgetForm, +) from project.forms.event import FindEventForm from project.forms.event_place import FindEventPlaceForm from project.models import ( @@ -37,6 +41,7 @@ from project.services.admin_unit import ( get_admin_unit_member_invitations, get_admin_unit_organization_invitations, get_admin_unit_query, + get_member_for_admin_unit_by_user_id, ) from project.services.event import get_events_query from project.services.event_search import EventSearchParams @@ -49,6 +54,7 @@ from project.views.utils import ( get_current_admin_unit, get_pagination_urls, handleSqlError, + non_match_for_deletion, permission_missing, set_current_admin_unit, ) @@ -280,6 +286,54 @@ def manage_admin_unit_members(id): ) +@app.route("/manage/admin_unit//membership/delete", methods=("GET", "POST")) +@auth_required() +def manage_admin_unit_delete_membership(id): + admin_unit = get_admin_unit_for_manage_or_404(id) + set_current_admin_unit(admin_unit) + + member = get_member_for_admin_unit_by_user_id( + admin_unit.id, + current_user.id, + ) + + if not member: + # E.g. global admin + flash(gettext("You are not a member of this organization"), "danger") + return redirect(url_for("manage_admin_unit_members", id=id)) + + if not can_current_user_delete_member(member): + flash( + gettext("Last remaining administrator can not leave the organization."), + "danger", + ) + return redirect(url_for("manage_admin_unit_members", id=id)) + + form = AdminUnitDeleteMembershipForm() + + if form.validate_on_submit(): + if non_match_for_deletion(form.name.data, admin_unit.name): + flash(gettext("Entered name does not match organization name"), "danger") + else: + try: + db.session.delete(member) + db.session.commit() + flash(gettext("Organization successfully left"), "success") + return redirect(url_for("manage_admin_units")) + except SQLAlchemyError as e: + db.session.rollback() + flash(handleSqlError(e), "danger") + else: + flash_errors(form) + + return render_template( + "manage/delete_membership.html", + admin_unit=admin_unit, + member=member, + form=form, + ) + + @app.route("/manage/admin_unit//relations") @app.route("/manage/admin_unit//relations/") @auth_required() diff --git a/tests/seeder.py b/tests/seeder.py index bd4d0df..f599a28 100644 --- a/tests/seeder.py +++ b/tests/seeder.py @@ -121,7 +121,12 @@ class Seeder(object): verify=True, ) - def create_admin_unit_member(self, admin_unit_id, role_names): + def create_admin_unit_member( + self, + admin_unit_id, + role_names, + email="test@test.de", + ): from project.services.admin_unit import ( add_user_to_admin_unit_with_roles, get_admin_unit_by_id, @@ -129,7 +134,7 @@ class Seeder(object): from project.services.user import get_user with self._app.app_context(): - user_id = self.create_user() + user_id = self.create_user(email=email) user = get_user(user_id) admin_unit = get_admin_unit_by_id(admin_unit_id) member = add_user_to_admin_unit_with_roles(user, admin_unit, role_names) @@ -188,8 +193,12 @@ class Seeder(object): if remove_favorite_event(user_id, event_id): self._db.session.commit() - def create_admin_unit_member_event_verifier(self, admin_unit_id): - return self.create_admin_unit_member(admin_unit_id, ["event_verifier"]) + def create_admin_unit_member_event_verifier( + self, + admin_unit_id, + email="test@test.de", + ): + return self.create_admin_unit_member(admin_unit_id, ["event_verifier"], email) def upsert_event_place(self, admin_unit_id, name, location=None): from project.services.place import upsert_event_place diff --git a/tests/utils.py b/tests/utils.py index 2dbe28b..a592042 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -246,8 +246,8 @@ class UtilActions(object): url = self.get_image_url(image, **values) return url - def get(self, url): - response = self._client.get(url) + def get(self, url, **kwargs): + response = self._client.get(url, **kwargs) if response.status_code == 200: self._ajax_csrf = self.get_ajax_csrf(response) diff --git a/tests/views/test_admin_unit_member.py b/tests/views/test_admin_unit_member.py index 25fc2ce..b2a75ca 100644 --- a/tests/views/test_admin_unit_member.py +++ b/tests/views/test_admin_unit_member.py @@ -1,3 +1,9 @@ +import pytest + +from tests.seeder import Seeder +from tests.utils import UtilActions + + def test_update(client, app, utils, seeder): seeder.create_user() user_id = utils.login() @@ -63,14 +69,25 @@ def test_update_permission_missing(client, app, db, utils, seeder): assert response.status_code == 302 -def test_delete(client, app, utils, seeder): +@pytest.mark.parametrize("scenario", ["default", "current_user"]) +def test_delete(client, app, db, utils: UtilActions, seeder: Seeder, scenario: str): seeder.create_user() user_id = utils.login() admin_unit_id = seeder.create_admin_unit(user_id, "Meine Crew") - member_id = seeder.create_admin_unit_member_event_verifier(admin_unit_id) + member_email = "test@test.de" if scenario == "current_user" else "member@test.de" + member_id = seeder.create_admin_unit_member_event_verifier( + admin_unit_id, email=member_email + ) url = "/manage/member/%d/delete" % member_id response = client.get(url) + + if scenario == "current_user": + utils.assert_response_redirect( + response, "manage_admin_unit_delete_membership", id=admin_unit_id + ) + return + assert response.status_code == 200 with client: @@ -78,7 +95,7 @@ def test_delete(client, app, utils, seeder): url, data={ "csrf_token": utils.get_csrf(response), - "email": "Test@test.de", + "email": "member@test.de", "submit": "Submit", }, ) @@ -95,7 +112,9 @@ def test_delete_db_error(client, app, utils, seeder, mocker): seeder.create_user() user_id = utils.login() admin_unit_id = seeder.create_admin_unit(user_id, "Meine Crew") - member_id = seeder.create_admin_unit_member_event_verifier(admin_unit_id) + member_id = seeder.create_admin_unit_member_event_verifier( + admin_unit_id, email="member@test.de" + ) url = "/manage/member/%d/delete" % member_id response = client.get(url) @@ -108,7 +127,7 @@ def test_delete_db_error(client, app, utils, seeder, mocker): url, data={ "csrf_token": utils.get_csrf(response), - "email": "test@test.de", + "email": "member@test.de", "submit": "Submit", }, ) @@ -121,7 +140,9 @@ def test_delete_email_does_not_match(client, app, utils, seeder): seeder.create_user() user_id = utils.login() admin_unit_id = seeder.create_admin_unit(user_id, "Meine Crew") - member_id = seeder.create_admin_unit_member_event_verifier(admin_unit_id) + member_id = seeder.create_admin_unit_member_event_verifier( + admin_unit_id, email="member@test.de" + ) url = "/manage/member/%d/delete" % member_id response = client.get(url) diff --git a/tests/views/test_manage.py b/tests/views/test_manage.py index 97cecd4..37c480d 100644 --- a/tests/views/test_manage.py +++ b/tests/views/test_manage.py @@ -1,5 +1,8 @@ import pytest +from tests.seeder import Seeder +from tests.utils import UtilActions + def test_index_noCookie(client, seeder, utils): user_id, admin_unit_id = seeder.setup_base() @@ -248,3 +251,85 @@ def test_verification_requests_outgoing(client, seeder, utils): ) utils.assert_response_contains(response, "Stadtmarketing") utils.assert_response_contains(response, "Please give us a call") + + +@pytest.mark.parametrize("scenario", ["db_error", "default", "last_admin", "non_match"]) +def test_manage_admin_unit_delete_membership( + client, utils: UtilActions, seeder: Seeder, app, db, mocker, scenario: str +): + user_id, admin_unit_id = seeder.setup_base() + + with app.app_context(): + from project.services.admin_unit import get_member_for_admin_unit_by_user_id + + member = get_member_for_admin_unit_by_user_id( + admin_unit_id, + user_id, + ) + member_id = member.id + + if not scenario == "last_admin": + seeder.create_admin_unit_member( + admin_unit_id, ["admin"], "admin.member@test.de" + ) + + url = utils.get_url("manage_admin_unit_delete_membership", id=admin_unit_id) + + if scenario == "last_admin": + response = utils.get(url, follow_redirects=True) + utils.assert_response_error_message( + response, + "Der letzte verbleibende Administrator kann die Organisation nicht verlassen.", + ) + return + + response = utils.get_ok(url) + + if scenario == "db_error": + utils.mock_db_commit(mocker) + + form_name = "Meine Crew" + + if scenario == "non_match": + form_name = "wrong" + + response = utils.post_form( + url, + response, + { + "name": form_name, + }, + ) + + if scenario == "non_match": + utils.assert_response_error_message( + response, "Der eingegebene Name entspricht nicht dem Namen der Organisation" + ) + return + + if scenario == "db_error": + utils.assert_response_db_error(response) + return + + utils.assert_response_redirect(response, "manage_admin_units") + + with app.app_context(): + from project.models import AdminUnitMember + + assert db.session.get(AdminUnitMember, member_id) is None + + +def test_manage_admin_unit_delete_membership_no_member( + client, utils: UtilActions, seeder: Seeder, app, db +): + user_id, admin_unit_id = seeder.setup_base(admin=True) + other_user_id, other_admin_unit_id = seeder.setup_base( + log_in=False, email="other@test.de", name="Other Crew" + ) + + url = utils.get_url("manage_admin_unit_delete_membership", id=other_admin_unit_id) + response = utils.get(url, follow_redirects=True) + utils.assert_response_error_message( + response, + "Du bist kein Mitglied dieser Organisation", + )