Browse Source

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

master
François Poulain François Poulain 2 months ago
parent
commit
b1b6ba17cd
5 changed files with 127 additions and 21 deletions
  1. +31
    -4
      benevalibre/association/models.py
  2. +69
    -14
      benevalibre/association/tests/test_views.py
  3. +13
    -1
      benevalibre/association/views.py
  4. +10
    -0
      benevalibre/templates/cruditor/delete.html
  5. +4
    -2
      docs/source/conf.py

+ 31
- 4
benevalibre/association/models.py View File

@@ -480,9 +480,6 @@ class Engagement(HTMLDocString, AbstractAssociated):
super().save(*args, **kwargs)

def clean(self):
# FIXME: une association ne devrait pas pouvoir perdre son dernier
# dirigeant

errors = []

if self.association_id:
@@ -510,9 +507,39 @@ class Engagement(HTMLDocString, AbstractAssociated):
"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:
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):
return "{} est {}".format(self.user, self.role)

+ 69
- 14
benevalibre/association/tests/test_views.py View File

@@ -36,14 +36,25 @@ class TestAssociationManagementButtons:
'role_name, expected',
[('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.role = association.role_set.get(name=role_name)
engagement.save()

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.list_users = True
role.save()
@@ -610,7 +621,7 @@ class TestEngagement(ManagerOnlyViewMixin):
assert len(table_row) == 1
assert "Dirigeant" in str(table_row[-1])

# 2. on modifie notre rôle
# 2. on échoue à modifier notre rôle
url = (
bs(response.content, 'html.parser')
.find('tbody')
@@ -621,22 +632,38 @@ class TestEngagement(ManagerOnlyViewMixin):
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) == 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 "a été modifié" in messages[0]

table_row = (
bs(response.content, 'html.parser').find('tbody').find_all('tr')
)
assert len(table_row) == 1
assert "Animat⋅eur⋅rice" in str(table_row[-1])
assert len(table_row) == 2
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 = (
bs(response.content, 'html.parser')
.find('tbody')
.find_all('a', class_='update-link')[-1]
.find_all('a', class_='update-link')[-2]
.attrs['href']
)

data = {'role': dirigeant_role.id}
response = client.post(url, data, follow=True)
assert response.status_code == 200
@@ -653,10 +680,7 @@ class TestEngagement(ManagerOnlyViewMixin):
str(animateur_role.id),
]

# 4 on tente de modifier un dirigeant mais on n'est pas en capacité
engagement = self.association.engagement_set.create(
user=foreign, role=dirigeant_role
)
# 5. on tente de modifier un dirigeant mais on n'est pas en capacité
url2 = reverse(
'association:engagement:update',
args=[self.association.id, engagement.id],
@@ -672,7 +696,7 @@ class TestEngagement(ManagerOnlyViewMixin):
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.force_login(foreign)
data = {'role': dirigeant_role.id}
@@ -682,7 +706,7 @@ class TestEngagement(ManagerOnlyViewMixin):
assert user.get_short_name() in mailoutbox[0].body
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)

assert self.association.can_manage_engagements(user)
@@ -691,7 +715,38 @@ class TestEngagement(ManagerOnlyViewMixin):
response = client.post(
reverse(
'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,
follow=True,


+ 13
- 1
benevalibre/association/views.py View File

@@ -631,4 +631,16 @@ class EngagementUpdate(


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

+ 10
- 0
benevalibre/templates/cruditor/delete.html View File

@@ -1,7 +1,17 @@
{% extends "cruditor/delete.html" %}
{% load i18n %}

{% block form_actions_right %}
<button class="btn btn-outline-danger" type="submit" name="save">
{{ form_save_button_label|default:"Confirmer la suppression" }}
</button>
{% 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 %}

+ 4
- 2
docs/source/conf.py View File

@@ -8,9 +8,11 @@ http://www.sphinx-doc.org/en/master/config
import os
import sys
import django

sys.path.insert(0, os.path.abspath('../..'))
from benevalibre.settings import DJANGO_SETTINGS_MODULE
os.environ.setdefault('DJANGO_SETTINGS_MODULE', DJANGO_SETTINGS_MODULE)
from benevalibre.settings import DJANGO_SETTINGS_MODULE

os.environ.setdefault('DJANGO_SETTINGS_MODULE', DJANGO_SETTINGS_MODULE)
django.setup()




Loading…
Cancel
Save