Skip to content

Commit

Permalink
Permettre à l'AC de clôturer quand il est le dernier sans fin de suivi
Browse files Browse the repository at this point in the history
Lorsqu'une structure de l'AC est la seule qui n'a pas encore signalée la fin de suivi pour un événement, il y a ajout automatiquement de cette fin de suivi et l'événement est clôturé.
  • Loading branch information
alanzirek committed Feb 25, 2025
1 parent 2e0bf29 commit 818421a
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 25 deletions.
19 changes: 18 additions & 1 deletion core/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,24 @@ def can_publish(self, user):
return user.agent.is_in_structure(self.createur) if self.is_draft else False

def can_be_cloturer_by(self, user):
return not self.is_draft and not self.is_already_cloturer() and user.agent.structure.is_ac
return user.agent.structure.is_ac

def is_the_only_remaining_structure(self, user, contacts_not_in_fin_suivi) -> bool:
"""Un seul contact sans fin de suivi qui appartient à la structure de l'utilisateur"""
return len(contacts_not_in_fin_suivi) == 1 and contacts_not_in_fin_suivi[0].structure == user.agent.structure

def can_be_cloturer(self, user, contacts_not_in_fin_suivi) -> bool:
if self.is_draft or self.is_already_cloturer() or not self.can_be_cloturer_by(user):
return False

if not contacts_not_in_fin_suivi:
return True

if self.is_the_only_remaining_structure(user, contacts_not_in_fin_suivi):
return True

# Plusieurs contacts sans fin de suivi
return False

def is_already_cloturer(self):
return self.etat == self.Etat.CLOTURE
Expand Down
17 changes: 17 additions & 0 deletions sv/models/evenements.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,20 @@ def get_soft_delete_permission_error_message(self):

def get_soft_delete_attribute_error_message(self):
return f"L'évènement {self.numero} ne peut pas être supprimé"

def add_fin_suivi(self, user):
with transaction.atomic():
fin_suivi_contact = FinSuiviContact(
content_object=self,
contact=Contact.objects.get(structure=user.agent.structure),
)
fin_suivi_contact.full_clean()
fin_suivi_contact.save()

Message.objects.create(
title="Fin de suivi",
content="Fin de suivi ajoutée automatiquement suite à la clôture de l'événement.",
sender=user.agent.contact_set.get(),
message_type=Message.FIN_SUIVI,
content_object=self,
)
8 changes: 4 additions & 4 deletions sv/templates/sv/_cloturer_modal.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ <h1 id="fr-modal-cloturer-evenement-title" class="fr-modal__title">
<span class="fr-icon-arrow-right-line fr-icon--lg"></span>
Clôturer un événement
</h1>
{% if can_cloturer_evenement %}
{% if is_evenement_can_be_cloturer %}
<p>Étes-vous sûr.e de vouloir clôturer l'événement {{ evenement.numero }} ?</p>
{% else %}
<p>Vous ne pouvez pas clôturer l'événement n°{{ evenement.numero }} car les structures suivantes n'ont pas signalées la fin de suivi : </p>
<ul>
<p>Vous ne pouvez pas clôturer l'événement n°{{ evenement.numero }} car les structures suivantes n'ont pas signalé la fin de suivi : </p>
<ul data-testid="structures-not-in-fin-suivi">
{% for contact in contacts_not_in_fin_suivi %}
<li>{{ contact.structure }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% if can_cloturer_evenement %}
{% if is_evenement_can_be_cloturer %}
<div class="fr-modal__footer">
<div class="fr-btns-group fr-btns-group--right fr-btns-group--inline-lg fr-btns-group--icon-left">
<button class="fr-btn fr-btn--secondary" aria-controls="fr-modal-cloturer-evenement">
Expand Down
2 changes: 1 addition & 1 deletion sv/templates/sv/_evenement_action_navigation.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<li><a class="fr-translate__language fr-nav__link" href="#" data-fr-opened="false" aria-controls="fr-modal-edit-visibilite"><span class="fr-icon-eye-fill fr-mr-2v fr-icon--sm" aria-hidden="true"></span>Modifier la visibilité</a></li>
{% endif %}
<li><a class="fr-translate__language fr-nav__link" href="{{ evenement.get_update_url}}" ><span class="fr-icon-edit-fill fr-mr-2v fr-icon--sm" aria-hidden="true"></span>Modifier l'événement</a></li>
{% if can_be_cloturer %}
{% if is_evenement_can_be_cloturer_by_user %}
<li><a class="fr-translate__language fr-nav__link" href="#" data-fr-opened="false" aria-controls="fr-modal-cloturer-evenement"><span class="fr-icon-close-circle-fill fr-mr-2v fr-icon--sm" aria-hidden="true"></span>Clôturer l'événement</a></li>
{% endif %}
<li><a class="fr-translate__language fr-nav__link" href="#" data-fr-opened="false" aria-controls="fr-modal-delete-evenement"><span class="fr-icon-close-circle-fill fr-mr-2v fr-icon--sm" aria-hidden="true"></span>Supprimer l'événement</a></li>
Expand Down
44 changes: 40 additions & 4 deletions sv/tests/test_evenement_etats.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from django.contrib.contenttypes.models import ContentType
from playwright.sync_api import Page, expect
from core.constants import AC_STRUCTURE, MUS_STRUCTURE
from core.models import Contact, FinSuiviContact
from core.models import Contact, FinSuiviContact, Message


@pytest.fixture
Expand Down Expand Up @@ -144,16 +144,17 @@ def test_cannot_cloturer_evenement_if_creator_structure_not_in_fin_suivi(
evenement = EvenementFactory()
mocked_authentification_user.agent.structure = contact_ac.structure
evenement.contacts.add(contact_ac)
evenement.contacts.add(ContactStructureFactory(structure=evenement.createur))

page.goto(f"{live_server.url}{evenement.get_absolute_url()}")
page.get_by_role("button", name="Actions").click()
page.get_by_role("link", name="Clôturer l'événement").click()

cloturer_element = page.get_by_label("Clôturer un événement")
expect(cloturer_element.get_by_role("paragraph")).to_contain_text(
f"Vous ne pouvez pas clôturer l'événement n°{evenement.numero} car les structures suivantes n'ont pas signalées la fin de suivi :"
f"Vous ne pouvez pas clôturer l'événement n°{evenement.numero} car les structures suivantes n'ont pas signalé la fin de suivi :"
)
expect(cloturer_element.get_by_role("listitem")).to_contain_text(contact_ac.structure.libelle)
expect(page.get_by_test_id("structures-not-in-fin-suivi")).to_contain_text(contact_ac.structure.libelle)
evenement.refresh_from_db()
assert evenement.etat == Evenement.Etat.EN_COURS

Expand Down Expand Up @@ -181,7 +182,7 @@ def test_cannot_cloturer_evenement_if_on_off_contacts_structures_not_in_fin_suiv
page.get_by_role("link", name="Clôturer l'événement").click()

expect(page.get_by_label("Clôturer un événement").get_by_role("paragraph")).to_contain_text(
f"Vous ne pouvez pas clôturer l'événement n°{evenement.numero} car les structures suivantes n'ont pas signalées la fin de suivi :"
f"Vous ne pouvez pas clôturer l'événement n°{evenement.numero} car les structures suivantes n'ont pas signalé la fin de suivi :"
)
expect(page.get_by_label("Clôturer un événement").get_by_role("listitem")).to_contain_text(
contact2.structure.libelle
Expand Down Expand Up @@ -211,3 +212,38 @@ def test_show_cloture_tag(live_server, page: Page):
evenement = EvenementFactory(etat=Evenement.Etat.CLOTURE)
page.goto(f"{live_server.url}{evenement.get_absolute_url()}")
expect(page.get_by_text("Clôturé")).to_be_visible()


def test_cloture_evenement_auto_fin_suivi_si_derniere_structure_ac(
live_server, page: Page, mocked_authentification_user
):
"""Test qu'une structure de l'AC peut clôturer un événement si elle est la dernière structure de la liste des contacts à ne pas avoir signalé la fin de suivi.
Dans ce cas, l'état 'fin de suivi' est ajouté à la structure de l'AC et un message fin de suivi est ajouté automatiquement."""
evenement = EvenementFactory()
contact_mus = ContactStructureFactory(
structure__niveau1=AC_STRUCTURE, structure__niveau2=MUS_STRUCTURE, structure__libelle=MUS_STRUCTURE
)
contact_1 = ContactStructureFactory()
evenement.contacts.set([contact_mus, contact_1])
evenement_content_type = ContentType.objects.get_for_model(evenement)
FinSuiviContact(content_type=evenement_content_type, object_id=evenement.id, contact=contact_1).save()
mocked_authentification_user.agent.structure = contact_mus.structure
mocked_authentification_user.agent.save()

page.goto(f"{live_server.url}{evenement.get_absolute_url()}")
page.get_by_role("button", name="Actions").click()
page.get_by_role("link", name="Clôturer l'événement").click()
page.get_by_role("button", name="Confirmer la clôture").click()

expect(page.get_by_text(f"L'événement n°{evenement.numero} a bien été clôturé.")).to_be_visible()
evenement.refresh_from_db()
assert evenement.etat == Evenement.Etat.CLOTURE
assert FinSuiviContact.objects.filter(
content_type=evenement_content_type, object_id=evenement.id, contact=contact_mus
).exists()
assert Message.objects.filter(
message_type=Message.FIN_SUIVI,
sender=mocked_authentification_user.agent.contact_set.get(),
content_type=evenement_content_type,
object_id=evenement.id,
).exists()
18 changes: 18 additions & 0 deletions sv/view_mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,21 @@ def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["prelevement_resultats"] = dict(Prelevement.Resultat.choices)
return context


class WithClotureContextMixin:
"""
Mixin qui ajoute au contexte les informations relatives à la clôture d'un événement.
"""

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
evenement = self.get_object()
context["contacts_not_in_fin_suivi"] = contacts_not_in_fin_suivi = (
evenement.get_contacts_structures_not_in_fin_suivi()
)
context["is_evenement_can_be_cloturer_by_user"] = evenement.can_be_cloturer_by(self.request.user)
context["is_evenement_can_be_cloturer"] = evenement.can_be_cloturer(
self.request.user, contacts_not_in_fin_suivi
)
return context
32 changes: 17 additions & 15 deletions sv/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
WithStatusToOrganismeNuisibleMixin,
WithAddUserContactsMixin,
WithPrelevementResultatsMixin,
WithClotureContextMixin,
)


Expand Down Expand Up @@ -100,6 +101,7 @@ class EvenementDetailView(
WithMessagesListInContextMixin,
WithContactListInContextMixin,
WithFreeLinksListInContextMixin,
WithClotureContextMixin,
UserPassesTestMixin,
DetailView,
):
Expand Down Expand Up @@ -155,7 +157,6 @@ def get_context_data(self, **kwargs):
context["can_publish"] = self.get_object().can_publish(self.request.user)
context["can_update_visibilite"] = self.get_object().can_update_visibilite(self.request.user)
context["visibilite_form"] = EvenementVisibiliteUpdateForm(obj=self.get_object())
context["can_be_cloturer"] = self.object.can_be_cloturer_by(self.request.user)
context["can_be_ac_notified"] = self.object.can_notifiy(self.request.user)
context["latest_version"] = self.object.latest_version
fiche_zone = self.get_object().fiche_zone_delimitee
Expand All @@ -165,10 +166,6 @@ def get_context_data(self, **kwargs):
(zone_infestee, zone_infestee.fichedetection_set.all())
for zone_infestee in fiche_zone.zoneinfestee_set.all()
]

contacts_not_in_fin_suivi = self.get_object().get_contacts_structures_not_in_fin_suivi()
context["contacts_not_in_fin_suivi"] = contacts_not_in_fin_suivi
context["can_cloturer_evenement"] = len(contacts_not_in_fin_suivi) == 0
context["message_form"] = MessageForm(
sender=self.request.user,
obj=self.get_object(),
Expand Down Expand Up @@ -432,28 +429,33 @@ def post(self, request):
class EvenementCloturerView(View):
def post(self, request, pk):
data = self.request.POST
content_type = ContentType.objects.get(pk=data["content_type_id"]).model_class()
evenement = content_type.objects.get(pk=pk)
content_type = ContentType.objects.get(pk=data["content_type_id"])
evenement = content_type.model_class().objects.get(pk=pk)
redirect_url = evenement.get_absolute_url()
if not evenement.can_be_cloturer_by(request.user):
messages.error(request, "Cet événement ne peut pas être clôturé.")
return redirect(redirect_url)

if evenement.is_already_cloturer():
messages.error(request, f"L'événement n°{evenement.numero} est déjà clôturé.")
return redirect(redirect_url)

if not evenement.can_be_cloturer_by(request.user):
messages.error(request, "Vous n'avez pas les droits pour clôturer cet événement.")
return redirect(redirect_url)

contacts_not_in_fin_suivi = evenement.get_contacts_structures_not_in_fin_suivi()
if contacts_not_in_fin_suivi:
if evenement.can_be_cloturer(self.request.user, contacts_not_in_fin_suivi):
if evenement.is_the_only_remaining_structure(self.request.user, contacts_not_in_fin_suivi):
evenement.add_fin_suivi(self.request.user)
evenement.cloturer()
messages.success(request, f"L'événement n°{evenement.numero} a bien été clôturé.")
return redirect(redirect_url)

if len(contacts_not_in_fin_suivi) > 1:
messages.error(
request,
f"L'événement n°{evenement.numero} ne peut pas être clôturé car les structures suivantes n'ont pas signalées la fin de suivi : {', '.join([str(contact) for contact in contacts_not_in_fin_suivi])}",
)
return redirect(redirect_url)

evenement.cloturer()
messages.success(request, f"L'événement n°{evenement.numero} a bien été clôturé.")
return redirect(redirect_url)


class EvenementVisibiliteUpdateView(CanUpdateVisibiliteRequiredMixin, SuccessMessageMixin, UpdateView):
model = Evenement
Expand Down

0 comments on commit 818421a

Please sign in to comment.