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 aff23f3..58114db 100644 Binary files a/project/translations/de/LC_MESSAGES/messages.mo and b/project/translations/de/LC_MESSAGES/messages.mo differ diff --git a/project/translations/de/LC_MESSAGES/messages.po b/project/translations/de/LC_MESSAGES/messages.po index 37ed8de..020d133 100644 --- a/project/translations/de/LC_MESSAGES/messages.po +++ b/project/translations/de/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: 2020-06-07 18:51+0200\n" "Last-Translator: FULL NAME \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 11a700d..c9b53b0 100644 Binary files a/project/translations/en/LC_MESSAGES/messages.mo and b/project/translations/en/LC_MESSAGES/messages.mo differ 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", + )