fix(engagement): empêche de se retirer d une asso si ça créé une association orpheline

pull/122/head
François Poulain 2019-12-14 14:24:01 +01:00 commité par François Poulain
Parent b9897febe0
révision b1b6ba17cd
5 fichiers modifiés avec 127 ajouts et 21 suppressions

Voir le fichier

@ -480,9 +480,6 @@ class Engagement(HTMLDocString, AbstractAssociated):
super().save(*args, **kwargs) super().save(*args, **kwargs)
def clean(self): def clean(self):
# FIXME: une association ne devrait pas pouvoir perdre son dernier
# dirigeant
errors = [] errors = []
if self.association_id: if self.association_id:
@ -510,9 +507,39 @@ class Engagement(HTMLDocString, AbstractAssociated):
"Modifiez le rôle existant plutôt que d'en ajouter un." "Modifiez le rôle existant plutôt que d'en ajouter un."
) )
) )
if (
self.id
and self.role
and not self.role.manage_association
and not Engagement.objects.filter(
~models.Q(id=self.id),
association_id=self.association_id,
role__manage_association=True,
).exists()
):
errors.append(
ValidationError(
"Modifier cet engagement rendrait impossible la "
"gestion de l'association. Avant ça, vous devez "
"donner le rôle de dirigeant à une autre personne.",
)
)
if errors: if errors:
raise ValidationError(errors) raise ValidationError(errors)
def delete(self):
if not Engagement.objects.filter(
~models.Q(id=self.id),
association_id=self.association_id,
role__manage_association=True,
).exists():
raise ValidationError(
"Supprimer cet engagement rendrait impossible la "
"gestion de l'association. Avant ça, vous devez "
"donner le rôle de dirigeant à une autre personne.",
)
return super().delete()
def __str__(self): def __str__(self):
return "{} est {}".format(self.user, self.role) return "{} est {}".format(self.user, self.role)

Voir le fichier

@ -36,14 +36,25 @@ class TestAssociationManagementButtons:
'role_name, expected', 'role_name, expected',
[('Bénévole', 0), ('Animat⋅eur⋅rice', 5), ('Dirigeant', 7)], [('Bénévole', 0), ('Animat⋅eur⋅rice', 5), ('Dirigeant', 7)],
) )
def test_role(self, association, user, engagement, role_name, expected): def test_role(
self, association, user, foreign, engagement, role_name, expected
):
# upgrade foreign pour avoir au moins toujours un dirigeant
role = association.role_set.get_dirigeant()
association.engagement_set.create(user=foreign, role=role)
# bidouille l'engagement
engagement = association.get_engagement(user) engagement = association.get_engagement(user)
engagement.role = association.role_set.get(name=role_name) engagement.role = association.role_set.get(name=role_name)
engagement.save() engagement.save()
assert len(get_management_buttons(association, user)) == expected assert len(get_management_buttons(association, user)) == expected
def test_role_list_users(self, association, user, engagement): def test_role_list_users(self, association, user, foreign, engagement):
# upgrade foreign pour avoir au moins toujours un dirigeant
role = association.role_set.get_dirigeant()
association.engagement_set.create(user=foreign, role=role)
role = association.role_set.get_benevole() role = association.role_set.get_benevole()
role.list_users = True role.list_users = True
role.save() role.save()
@ -610,7 +621,7 @@ class TestEngagement(ManagerOnlyViewMixin):
assert len(table_row) == 1 assert len(table_row) == 1
assert "Dirigeant" in str(table_row[-1]) assert "Dirigeant" in str(table_row[-1])
# 2. on modifie notre rôle # 2. on échoue à modifier notre rôle
url = ( url = (
bs(response.content, 'html.parser') bs(response.content, 'html.parser')
.find('tbody') .find('tbody')
@ -621,22 +632,38 @@ class TestEngagement(ManagerOnlyViewMixin):
response = client.post(url, data, follow=True) response = client.post(url, data, follow=True)
assert response.status_code == 200 assert response.status_code == 200
messages = [m.message for m in get_messages(response.wsgi_request)] messages = [m.message for m in get_messages(response.wsgi_request)]
assert len(messages) == 0
assert count_text_in_content(response, "rendrait impossible la gesti")
# 3. on modifie notre rôle
# 3.1 on créé un nouveau dirigeant
engagement = self.association.engagement_set.create(
user=foreign, role=dirigeant_role
)
# 3.2 on arrive à modifier notre rôle
data = {'role': animateur_role.id}
response = client.post(url, data, follow=True)
assert response.status_code == 200
messages = [m.message for m in get_messages(response.wsgi_request)]
assert len(messages) == 1 assert len(messages) == 1
assert "a été modifié" in messages[0] assert "a été modifié" in messages[0]
table_row = ( table_row = (
bs(response.content, 'html.parser').find('tbody').find_all('tr') bs(response.content, 'html.parser').find('tbody').find_all('tr')
) )
assert len(table_row) == 1 assert len(table_row) == 2
assert "Animat⋅eur⋅rice" in str(table_row[-1]) assert "Animat⋅eur⋅rice" in str(table_row[-2])
# 3. on tente de redevenir dirigeant mais on n'est plus en capacité # 4. on tente de redevenir dirigeant mais on n'est plus en capacité
url = ( url = (
bs(response.content, 'html.parser') bs(response.content, 'html.parser')
.find('tbody') .find('tbody')
.find_all('a', class_='update-link')[-1] .find_all('a', class_='update-link')[-2]
.attrs['href'] .attrs['href']
) )
data = {'role': dirigeant_role.id} data = {'role': dirigeant_role.id}
response = client.post(url, data, follow=True) response = client.post(url, data, follow=True)
assert response.status_code == 200 assert response.status_code == 200
@ -653,10 +680,7 @@ class TestEngagement(ManagerOnlyViewMixin):
str(animateur_role.id), str(animateur_role.id),
] ]
# 4 on tente de modifier un dirigeant mais on n'est pas en capacité # 5. on tente de modifier un dirigeant mais on n'est pas en capacité
engagement = self.association.engagement_set.create(
user=foreign, role=dirigeant_role
)
url2 = reverse( url2 = reverse(
'association:engagement:update', 'association:engagement:update',
args=[self.association.id, engagement.id], args=[self.association.id, engagement.id],
@ -672,7 +696,7 @@ class TestEngagement(ManagerOnlyViewMixin):
str(dirigeant_role.id), str(dirigeant_role.id),
] ]
# 5 un dirigeant nous modifie et on reçoit une notification # 6. un dirigeant nous modifie et on reçoit une notification
client.logout() client.logout()
client.force_login(foreign) client.force_login(foreign)
data = {'role': dirigeant_role.id} data = {'role': dirigeant_role.id}
@ -682,7 +706,7 @@ class TestEngagement(ManagerOnlyViewMixin):
assert user.get_short_name() in mailoutbox[0].body assert user.get_short_name() in mailoutbox[0].body
assert "Mise à jour de votre engagement" in mailoutbox[0].subject assert "Mise à jour de votre engagement" in mailoutbox[0].subject
def test_self_erase_go_back_to_home(self, client, user, manager): def test_self_erase_do_not_create_orpheans(self, client, user, manager):
client.force_login(user) client.force_login(user)
assert self.association.can_manage_engagements(user) assert self.association.can_manage_engagements(user)
@ -691,7 +715,38 @@ class TestEngagement(ManagerOnlyViewMixin):
response = client.post( response = client.post(
reverse( reverse(
'association:engagement:delete', 'association:engagement:delete',
args=[self.association.id, manager.id], args=[self.association.id, user.engagement_set.first().id],
),
data,
follow=True,
)
assert response.status_code == 200
assert (
response.resolver_match.view_name
== 'association:engagement:delete'
)
assert count_text_in_content(response, "Impossible de supprimer")
assert count_text_in_content(
response, "rendrait impossible la gestion"
)
def test_self_erase_go_back_to_home(self, client, foreign, user, manager):
# we need to define the next leader before leaving
r = self.association.role_set.get_dirigeant()
self.association.engagement_set.create(user=foreign, role=r)
assert self.association.can_manage_engagements(user)
assert self.association.can_manage_engagements(foreign)
client.force_login(user)
data = {'confirm': True}
response = client.post(
reverse(
'association:engagement:delete',
args=[self.association.id, user.engagement_set.first().id],
), ),
data, data,
follow=True, follow=True,

Voir le fichier

@ -631,4 +631,16 @@ class EngagementUpdate(
class EngagementDelete(EngagementViewMixin, CruditorDeleteView): class EngagementDelete(EngagementViewMixin, CruditorDeleteView):
pass def delete(self, request, *args, **kwargs):
"""
Call ``perform_delete`` method and redirect to the success URL with a
nice success message. If there are protected related objects, an error
message is shown instead with the output of ``format_linked_objects``.
"""
self.object = self.get_object()
try:
return super().delete(request, *args, **kwargs)
except models.ValidationError as e:
return self.render_to_response(
self.get_context_data(error_message=e.message)
)

Voir le fichier

@ -1,7 +1,17 @@
{% extends "cruditor/delete.html" %} {% extends "cruditor/delete.html" %}
{% load i18n %}
{% block form_actions_right %} {% block form_actions_right %}
<button class="btn btn-outline-danger" type="submit" name="save"> <button class="btn btn-outline-danger" type="submit" name="save">
{{ form_save_button_label|default:"Confirmer la suppression" }} {{ form_save_button_label|default:"Confirmer la suppression" }}
</button> </button>
{% endblock %} {% endblock %}
{% block content %}
{% if error_message %}
<div class="alert alert-danger">{% trans "Unable to delete this item." %}</div>
<p>{{ error_message }}</p>
{% else %}
{{ block.super }}
{% endif %}
{% endblock %}

Voir le fichier

@ -8,9 +8,11 @@ http://www.sphinx-doc.org/en/master/config
import os import os
import sys import sys
import django import django
sys.path.insert(0, os.path.abspath('../..')) sys.path.insert(0, os.path.abspath('../..'))
from benevalibre.settings import DJANGO_SETTINGS_MODULE from benevalibre.settings import DJANGO_SETTINGS_MODULE
os.environ.setdefault('DJANGO_SETTINGS_MODULE', DJANGO_SETTINGS_MODULE)
os.environ.setdefault('DJANGO_SETTINGS_MODULE', DJANGO_SETTINGS_MODULE)
django.setup() django.setup()