Merge pull request #459 from eventcally/issues/458

Leave organization #458
This commit is contained in:
Daniel Grams 2023-05-03 23:28:42 +02:00 committed by GitHub
commit b93c16c797
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 360 additions and 72 deletions

View File

@ -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

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -198,24 +198,24 @@ msgstr ""
msgid "You have received an invitation" msgid "You have received an invitation"
msgstr "" 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 #: project/views/root.py:55
msgid "Terms of service" msgid "Terms of service"
msgstr "" 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 #: project/views/root.py:63
msgid "Legal notice" msgid "Legal notice"
msgstr "" msgstr ""
#: project/forms/admin.py:13 project/templates/_macros.html:1473 #: 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/templates/widget/event_suggestion/create.html:204
#: project/views/admin_unit.py:83 project/views/root.py:71 #: project/views/admin_unit.py:83 project/views/root.py:71
msgid "Contact" msgid "Contact"
msgstr "" 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 #: project/views/root.py:79
msgid "Privacy" msgid "Privacy"
msgstr "" msgstr ""
@ -310,11 +310,11 @@ msgstr ""
#: project/forms/admin.py:69 project/forms/admin_unit.py:28 #: 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/admin_unit.py:134 project/forms/admin_unit.py:139
#: project/forms/event.py:85 project/forms/event.py:114 #: project/forms/admin_unit.py:144 project/forms/event.py:85
#: project/forms/event_place.py:25 project/forms/event_place.py:50 #: project/forms/event.py:114 project/forms/event_place.py:25
#: project/forms/event_suggestion.py:26 project/forms/oauth2_client.py:66 #: project/forms/event_place.py:50 project/forms/event_suggestion.py:26
#: project/forms/organizer.py:25 project/forms/organizer.py:52 #: project/forms/oauth2_client.py:66 project/forms/organizer.py:25
#: project/forms/reference_request.py:23 #: project/forms/organizer.py:52 project/forms/reference_request.py:23
#: project/templates/admin/admin_units.html:19 #: project/templates/admin/admin_units.html:19
#: project/templates/event_place/list.html:19 #: project/templates/event_place/list.html:19
#: project/templates/manage/organizers.html:18 #: project/templates/manage/organizers.html:18
@ -487,6 +487,11 @@ msgstr ""
msgid "Cancel deletion" msgid "Cancel deletion"
msgstr "" 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 #: project/forms/admin_unit_member.py:13
msgid "Invite" msgid "Invite"
msgstr "" msgstr ""
@ -1142,6 +1147,7 @@ msgstr ""
#: project/templates/admin_unit/request_deletion.html:15 #: project/templates/admin_unit/request_deletion.html:15
#: project/templates/admin_unit/update.html:36 #: project/templates/admin_unit/update.html:36
#: project/templates/layout.html:247 #: project/templates/layout.html:247
#: project/templates/manage/delete_membership.html:13
msgid "Organization" msgid "Organization"
msgstr "" msgstr ""
@ -1668,7 +1674,7 @@ msgid "Switch organization"
msgstr "" msgstr ""
#: project/templates/developer/read.html:4 #: 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 #: project/templates/profile.html:46
msgid "Developer" msgid "Developer"
msgstr "" msgstr ""
@ -2262,7 +2268,7 @@ msgid "Organization successfully updated"
msgstr "" msgstr ""
#: project/views/admin.py:85 project/views/admin_unit.py:187 #: 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" msgid "Entered name does not match organization name"
msgstr "" msgstr ""
@ -2270,7 +2276,7 @@ msgstr ""
msgid "Organization successfully deleted" msgid "Organization successfully deleted"
msgstr "" 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 #: project/views/user.py:41
msgid "Settings successfully updated" msgid "Settings successfully updated"
msgstr "" msgstr ""
@ -2323,11 +2329,15 @@ msgstr ""
msgid "Member successfully updated" msgid "Member successfully updated"
msgstr "" 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" msgid "Entered email does not match member email"
msgstr "" msgstr ""
#: project/views/admin_unit_member.py:74 #: project/views/admin_unit_member.py:84
msgid "Member successfully deleted" msgid "Member successfully deleted"
msgstr "" msgstr ""
@ -2420,6 +2430,14 @@ msgstr ""
msgid "Places of Google Maps" msgid "Places of Google Maps"
msgstr "" 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 #: project/views/oauth2_client.py:37
msgid "OAuth2 client successfully created" msgid "OAuth2 client successfully created"
msgstr "" msgstr ""

View File

@ -8,6 +8,7 @@ from sqlalchemy import and_
from project import app from project import app
from project.models import AdminUnit, AdminUnitMember, Event, PublicStatus, User 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 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 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"): # pragma: no cover
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
)

View File

@ -137,3 +137,8 @@ class RequestAdminUnitDeletionForm(FlaskForm):
class CancelAdminUnitDeletionForm(FlaskForm): class CancelAdminUnitDeletionForm(FlaskForm):
submit = SubmitField(lazy_gettext("Cancel deletion")) submit = SubmitField(lazy_gettext("Cancel deletion"))
name = StringField(lazy_gettext("Name"), validators=[DataRequired()]) name = StringField(lazy_gettext("Name"), validators=[DataRequired()])
class AdminUnitDeleteMembershipForm(FlaskForm):
submit = SubmitField(lazy_gettext("Leave organization"))
name = StringField(lazy_gettext("Name"), validators=[DataRequired()])

View File

@ -273,6 +273,7 @@
</a> </a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarAdminUnitSwitchDropdown"> <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarAdminUnitSwitchDropdown">
<a class="dropdown-item" href="{{ url_for('manage_admin_units') }}">{{ _('Switch organization') }}</a> <a class="dropdown-item" href="{{ url_for('manage_admin_units') }}">{{ _('Switch organization') }}</a>
<a class="dropdown-item" href="{{ url_for('manage_admin_unit_delete_membership', id=current_admin_unit.id) }}">{{ _('Leave organization') }}&hellip;</a>
</div> </div>
</li> </li>
</div> </div>

View File

@ -0,0 +1,24 @@
{% extends "layout.html" %}
{% from "_macros.html" import render_field_with_errors, render_field %}
{% block content %}
<h1>{{ _('Leave organization') }} &quot;{{ admin_unit.name }}&quot;</h1>
<form action="" method="POST">
{{ form.hidden_tag() }}
<div class="card mb-4">
<div class="card-header">
{{ _('Organization') }}
</div>
<div class="card-body">
{{ render_field_with_errors(form.name) }}
</div>
</div>
{{ render_field(form.submit) }}
</form>
{% endblock %}

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\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" "PO-Revision-Date: 2020-06-07 18:51+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: de\n" "Language: de\n"
@ -199,24 +199,24 @@ msgstr "message"
msgid "You have received an invitation" msgid "You have received an invitation"
msgstr "Du hast eine Einladung erhalten" 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 #: project/views/root.py:55
msgid "Terms of service" msgid "Terms of service"
msgstr "Nutzungsbedingungen" 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 #: project/views/root.py:63
msgid "Legal notice" msgid "Legal notice"
msgstr "Impressum" msgstr "Impressum"
#: project/forms/admin.py:13 project/templates/_macros.html:1473 #: 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/templates/widget/event_suggestion/create.html:204
#: project/views/admin_unit.py:83 project/views/root.py:71 #: project/views/admin_unit.py:83 project/views/root.py:71
msgid "Contact" msgid "Contact"
msgstr "Kontakt" 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 #: project/views/root.py:79
msgid "Privacy" msgid "Privacy"
msgstr "Datenschutz" msgstr "Datenschutz"
@ -319,11 +319,11 @@ msgstr "Organisation löschen"
#: project/forms/admin.py:69 project/forms/admin_unit.py:28 #: 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/admin_unit.py:134 project/forms/admin_unit.py:139
#: project/forms/event.py:85 project/forms/event.py:114 #: project/forms/admin_unit.py:144 project/forms/event.py:85
#: project/forms/event_place.py:25 project/forms/event_place.py:50 #: project/forms/event.py:114 project/forms/event_place.py:25
#: project/forms/event_suggestion.py:26 project/forms/oauth2_client.py:66 #: project/forms/event_place.py:50 project/forms/event_suggestion.py:26
#: project/forms/organizer.py:25 project/forms/organizer.py:52 #: project/forms/oauth2_client.py:66 project/forms/organizer.py:25
#: project/forms/reference_request.py:23 #: project/forms/organizer.py:52 project/forms/reference_request.py:23
#: project/templates/admin/admin_units.html:19 #: project/templates/admin/admin_units.html:19
#: project/templates/event_place/list.html:19 #: project/templates/event_place/list.html:19
#: project/templates/manage/organizers.html:18 #: project/templates/manage/organizers.html:18
@ -507,6 +507,11 @@ msgstr "Löschung beantragen"
msgid "Cancel deletion" msgid "Cancel deletion"
msgstr "Löschen abbrechen" 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 #: project/forms/admin_unit_member.py:13
msgid "Invite" msgid "Invite"
msgstr "Einladen" msgstr "Einladen"
@ -1190,6 +1195,7 @@ msgstr "Wochentage"
#: project/templates/admin_unit/request_deletion.html:15 #: project/templates/admin_unit/request_deletion.html:15
#: project/templates/admin_unit/update.html:36 #: project/templates/admin_unit/update.html:36
#: project/templates/layout.html:247 #: project/templates/layout.html:247
#: project/templates/manage/delete_membership.html:13
msgid "Organization" msgid "Organization"
msgstr "Organisation" msgstr "Organisation"
@ -1725,7 +1731,7 @@ msgid "Switch organization"
msgstr "Organisation wechseln" msgstr "Organisation wechseln"
#: project/templates/developer/read.html:4 #: 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 #: project/templates/profile.html:46
msgid "Developer" msgid "Developer"
msgstr "Entwickler" msgstr "Entwickler"
@ -2334,7 +2340,7 @@ msgid "Organization successfully updated"
msgstr "Organisation erfolgreich aktualisiert" msgstr "Organisation erfolgreich aktualisiert"
#: project/views/admin.py:85 project/views/admin_unit.py:187 #: 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" msgid "Entered name does not match organization name"
msgstr "Der eingegebene Name entspricht nicht dem Namen der Organisation" 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" msgid "Organization successfully deleted"
msgstr "Organisation erfolgreich gelöscht" 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 #: project/views/user.py:41
msgid "Settings successfully updated" msgid "Settings successfully updated"
msgstr "Einstellungen erfolgreich aktualisiert" msgstr "Einstellungen erfolgreich aktualisiert"
@ -2398,11 +2404,15 @@ msgstr "Löschung der Organisation beantragt"
msgid "Member successfully updated" msgid "Member successfully updated"
msgstr "Mitglied erfolgreich aktualisiert" 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" msgid "Entered email does not match member email"
msgstr "Die eingegebene Email passt nicht zur Email des Mitglieds" 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" msgid "Member successfully deleted"
msgstr "Mitglied erfolgreich gelöscht" msgstr "Mitglied erfolgreich gelöscht"
@ -2495,6 +2505,14 @@ msgstr "Orte der Organisation"
msgid "Places of Google Maps" msgid "Places of Google Maps"
msgstr "Orte von 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 #: project/views/oauth2_client.py:37
msgid "OAuth2 client successfully created" msgid "OAuth2 client successfully created"
msgstr "OAuth2 Client erfolgreich erstellt" msgstr "OAuth2 Client erfolgreich erstellt"
@ -2585,7 +2603,9 @@ msgstr ""
msgid "" msgid ""
"You are administrator of at least one organization. Cancel your " "You are administrator of at least one organization. Cancel your "
"membership to delete your account." "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 #: project/views/user.py:92 project/views/user.py:119
msgid "Entered email does not match your email" msgid "Entered email does not match your email"

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\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" "PO-Revision-Date: 2021-04-30 15:04+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n" "Language: en\n"
@ -199,24 +199,24 @@ msgstr ""
msgid "You have received an invitation" msgid "You have received an invitation"
msgstr "" 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 #: project/views/root.py:55
msgid "Terms of service" msgid "Terms of service"
msgstr "" 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 #: project/views/root.py:63
msgid "Legal notice" msgid "Legal notice"
msgstr "" msgstr ""
#: project/forms/admin.py:13 project/templates/_macros.html:1473 #: 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/templates/widget/event_suggestion/create.html:204
#: project/views/admin_unit.py:83 project/views/root.py:71 #: project/views/admin_unit.py:83 project/views/root.py:71
msgid "Contact" msgid "Contact"
msgstr "" 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 #: project/views/root.py:79
msgid "Privacy" msgid "Privacy"
msgstr "" msgstr ""
@ -311,11 +311,11 @@ msgstr ""
#: project/forms/admin.py:69 project/forms/admin_unit.py:28 #: 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/admin_unit.py:134 project/forms/admin_unit.py:139
#: project/forms/event.py:85 project/forms/event.py:114 #: project/forms/admin_unit.py:144 project/forms/event.py:85
#: project/forms/event_place.py:25 project/forms/event_place.py:50 #: project/forms/event.py:114 project/forms/event_place.py:25
#: project/forms/event_suggestion.py:26 project/forms/oauth2_client.py:66 #: project/forms/event_place.py:50 project/forms/event_suggestion.py:26
#: project/forms/organizer.py:25 project/forms/organizer.py:52 #: project/forms/oauth2_client.py:66 project/forms/organizer.py:25
#: project/forms/reference_request.py:23 #: project/forms/organizer.py:52 project/forms/reference_request.py:23
#: project/templates/admin/admin_units.html:19 #: project/templates/admin/admin_units.html:19
#: project/templates/event_place/list.html:19 #: project/templates/event_place/list.html:19
#: project/templates/manage/organizers.html:18 #: project/templates/manage/organizers.html:18
@ -488,6 +488,11 @@ msgstr ""
msgid "Cancel deletion" msgid "Cancel deletion"
msgstr "" 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 #: project/forms/admin_unit_member.py:13
msgid "Invite" msgid "Invite"
msgstr "" msgstr ""
@ -1143,6 +1148,7 @@ msgstr ""
#: project/templates/admin_unit/request_deletion.html:15 #: project/templates/admin_unit/request_deletion.html:15
#: project/templates/admin_unit/update.html:36 #: project/templates/admin_unit/update.html:36
#: project/templates/layout.html:247 #: project/templates/layout.html:247
#: project/templates/manage/delete_membership.html:13
msgid "Organization" msgid "Organization"
msgstr "" msgstr ""
@ -1676,7 +1682,7 @@ msgid "Switch organization"
msgstr "" msgstr ""
#: project/templates/developer/read.html:4 #: 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 #: project/templates/profile.html:46
msgid "Developer" msgid "Developer"
msgstr "" msgstr ""
@ -2270,7 +2276,7 @@ msgid "Organization successfully updated"
msgstr "" msgstr ""
#: project/views/admin.py:85 project/views/admin_unit.py:187 #: 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" msgid "Entered name does not match organization name"
msgstr "" msgstr ""
@ -2278,7 +2284,7 @@ msgstr ""
msgid "Organization successfully deleted" msgid "Organization successfully deleted"
msgstr "" 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 #: project/views/user.py:41
msgid "Settings successfully updated" msgid "Settings successfully updated"
msgstr "" msgstr ""
@ -2331,11 +2337,15 @@ msgstr ""
msgid "Member successfully updated" msgid "Member successfully updated"
msgstr "" 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" msgid "Entered email does not match member email"
msgstr "" msgstr ""
#: project/views/admin_unit_member.py:74 #: project/views/admin_unit_member.py:84
msgid "Member successfully deleted" msgid "Member successfully deleted"
msgstr "" msgstr ""
@ -2428,6 +2438,14 @@ msgstr ""
msgid "Places of Google Maps" msgid "Places of Google Maps"
msgstr "" 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 #: project/views/oauth2_client.py:37
msgid "OAuth2 client successfully created" msgid "OAuth2 client successfully created"
msgstr "" msgstr ""

View File

@ -1,6 +1,6 @@
from flask import flash, redirect, render_template, url_for from flask import flash, redirect, render_template, url_for
from flask_babel import gettext 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 sqlalchemy.exc import SQLAlchemyError
from project import app, db from project import app, db
@ -59,7 +59,10 @@ def manage_admin_unit_member_delete(id):
member = AdminUnitMember.query.get_or_404(id) member = AdminUnitMember.query.get_or_404(id)
admin_unit = member.adminunit admin_unit = member.adminunit
if not has_access(admin_unit, "admin_unit.members:delete"): 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"): # pragma: no cover
return permission_missing(url_for("manage_admin_unit", id=admin_unit.id)) return permission_missing(url_for("manage_admin_unit", id=admin_unit.id))
form = DeleteAdminUnitMemberForm() form = DeleteAdminUnitMemberForm()

View File

@ -17,12 +17,16 @@ from project import app, db, dump_org_path
from project.access import ( from project.access import (
access_or_401, access_or_401,
admin_unit_suggestions_enabled_or_404, admin_unit_suggestions_enabled_or_404,
can_current_user_delete_member,
get_admin_unit_for_manage_or_404, get_admin_unit_for_manage_or_404,
get_admin_units_for_manage, get_admin_units_for_manage,
has_access, has_access,
) )
from project.celery_tasks import dump_admin_unit_task 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 import FindEventForm
from project.forms.event_place import FindEventPlaceForm from project.forms.event_place import FindEventPlaceForm
from project.models import ( from project.models import (
@ -37,6 +41,7 @@ from project.services.admin_unit import (
get_admin_unit_member_invitations, get_admin_unit_member_invitations,
get_admin_unit_organization_invitations, get_admin_unit_organization_invitations,
get_admin_unit_query, get_admin_unit_query,
get_member_for_admin_unit_by_user_id,
) )
from project.services.event import get_events_query from project.services.event import get_events_query
from project.services.event_search import EventSearchParams from project.services.event_search import EventSearchParams
@ -49,6 +54,7 @@ from project.views.utils import (
get_current_admin_unit, get_current_admin_unit,
get_pagination_urls, get_pagination_urls,
handleSqlError, handleSqlError,
non_match_for_deletion,
permission_missing, permission_missing,
set_current_admin_unit, set_current_admin_unit,
) )
@ -280,6 +286,54 @@ def manage_admin_unit_members(id):
) )
@app.route("/manage/admin_unit/<int:id>/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/<int:id>/relations") @app.route("/manage/admin_unit/<int:id>/relations")
@app.route("/manage/admin_unit/<int:id>/relations/<path:path>") @app.route("/manage/admin_unit/<int:id>/relations/<path:path>")
@auth_required() @auth_required()

View File

@ -121,7 +121,12 @@ class Seeder(object):
verify=True, 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 ( from project.services.admin_unit import (
add_user_to_admin_unit_with_roles, add_user_to_admin_unit_with_roles,
get_admin_unit_by_id, get_admin_unit_by_id,
@ -129,7 +134,7 @@ class Seeder(object):
from project.services.user import get_user from project.services.user import get_user
with self._app.app_context(): with self._app.app_context():
user_id = self.create_user() user_id = self.create_user(email=email)
user = get_user(user_id) user = get_user(user_id)
admin_unit = get_admin_unit_by_id(admin_unit_id) admin_unit = get_admin_unit_by_id(admin_unit_id)
member = add_user_to_admin_unit_with_roles(user, admin_unit, role_names) 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): if remove_favorite_event(user_id, event_id):
self._db.session.commit() self._db.session.commit()
def create_admin_unit_member_event_verifier(self, admin_unit_id): def create_admin_unit_member_event_verifier(
return self.create_admin_unit_member(admin_unit_id, ["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): def upsert_event_place(self, admin_unit_id, name, location=None):
from project.services.place import upsert_event_place from project.services.place import upsert_event_place

View File

@ -246,8 +246,8 @@ class UtilActions(object):
url = self.get_image_url(image, **values) url = self.get_image_url(image, **values)
return url return url
def get(self, url): def get(self, url, **kwargs):
response = self._client.get(url) response = self._client.get(url, **kwargs)
if response.status_code == 200: if response.status_code == 200:
self._ajax_csrf = self.get_ajax_csrf(response) self._ajax_csrf = self.get_ajax_csrf(response)

View File

@ -1,3 +1,9 @@
import pytest
from tests.seeder import Seeder
from tests.utils import UtilActions
def test_update(client, app, utils, seeder): def test_update(client, app, utils, seeder):
seeder.create_user() seeder.create_user()
user_id = utils.login() user_id = utils.login()
@ -63,14 +69,25 @@ def test_update_permission_missing(client, app, db, utils, seeder):
assert response.status_code == 302 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() seeder.create_user()
user_id = utils.login() user_id = utils.login()
admin_unit_id = seeder.create_admin_unit(user_id, "Meine Crew") 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 url = "/manage/member/%d/delete" % member_id
response = client.get(url) 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 assert response.status_code == 200
with client: with client:
@ -78,7 +95,7 @@ def test_delete(client, app, utils, seeder):
url, url,
data={ data={
"csrf_token": utils.get_csrf(response), "csrf_token": utils.get_csrf(response),
"email": "Test@test.de", "email": "member@test.de",
"submit": "Submit", "submit": "Submit",
}, },
) )
@ -95,7 +112,9 @@ def test_delete_db_error(client, app, utils, seeder, mocker):
seeder.create_user() seeder.create_user()
user_id = utils.login() user_id = utils.login()
admin_unit_id = seeder.create_admin_unit(user_id, "Meine Crew") 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 url = "/manage/member/%d/delete" % member_id
response = client.get(url) response = client.get(url)
@ -108,7 +127,7 @@ def test_delete_db_error(client, app, utils, seeder, mocker):
url, url,
data={ data={
"csrf_token": utils.get_csrf(response), "csrf_token": utils.get_csrf(response),
"email": "test@test.de", "email": "member@test.de",
"submit": "Submit", "submit": "Submit",
}, },
) )
@ -121,7 +140,9 @@ def test_delete_email_does_not_match(client, app, utils, seeder):
seeder.create_user() seeder.create_user()
user_id = utils.login() user_id = utils.login()
admin_unit_id = seeder.create_admin_unit(user_id, "Meine Crew") 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 url = "/manage/member/%d/delete" % member_id
response = client.get(url) response = client.get(url)
@ -138,14 +159,3 @@ def test_delete_email_does_not_match(client, app, utils, seeder):
) )
assert response.status_code == 200 assert response.status_code == 200
assert b"Die eingegebene Email passt nicht zur Email" in response.data assert b"Die eingegebene Email passt nicht zur Email" in response.data
def test_delete_permission_missing(client, app, db, utils, seeder):
owner_id = seeder.create_user("owner@owner")
admin_unit_id = seeder.create_admin_unit(owner_id, "Other crew")
member_id = seeder.create_admin_unit_member_event_verifier(admin_unit_id)
utils.login()
url = "/manage/member/%d/delete" % member_id
response = client.get(url)
assert response.status_code == 302

View File

@ -1,5 +1,8 @@
import pytest import pytest
from tests.seeder import Seeder
from tests.utils import UtilActions
def test_index_noCookie(client, seeder, utils): def test_index_noCookie(client, seeder, utils):
user_id, admin_unit_id = seeder.setup_base() 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, "Stadtmarketing")
utils.assert_response_contains(response, "Please give us a call") 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",
)