Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ajout le l'état clôturé #262

Merged
merged 1 commit into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion core/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@ def get_context_data(self, **kwargs):
.order_by_structure_and_name()
)
context["contacts_structures"] = (
self.get_object().contacts.structures_only().services_deconcentres_first().order_by_structure_and_niveau2()
self.get_object()
.contacts.structures_only()
.services_deconcentres_first()
.order_by_structure_and_niveau2()
.select_related("structure")
)
context["content_type"] = ContentType.objects.get_for_model(self.get_object())
context["contacts_fin_suivi"] = Contact.objects.filter(finsuivicontact__in=self.get_object().fin_suivi.all())
Expand Down
6 changes: 6 additions & 0 deletions core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.contrib.auth import get_user_model
from .managers import DocumentQueryset, ContactQueryset
from django.apps import apps
from core.constants import AC_STRUCTURE

User = get_user_model()

Expand Down Expand Up @@ -52,6 +53,11 @@ class Meta:
def __str__(self):
return self.libelle

@property
def is_ac(self):
"Permet de savoir si la structure fait partie de l'administration centrale (AC)"
return self.niveau1 == AC_STRUCTURE


class Contact(models.Model):
class Meta:
Expand Down
6 changes: 6 additions & 0 deletions sv/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@
class FicheDetectionManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_deleted=False)

def get_contacts_structures_not_in_fin_suivi(self, fiche_detection):
contacts_structure_fiche = fiche_detection.contacts.exclude(structure__isnull=True).select_related("structure")
fin_suivi_contacts_ids = fiche_detection.fin_suivi.values_list("contact", flat=True)
contacts_not_in_fin_suivi = contacts_structure_fiche.exclude(id__in=fin_suivi_contacts_ids)
return contacts_not_in_fin_suivi
23 changes: 22 additions & 1 deletion sv/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,10 @@ def __str__(self):


class Etat(models.Model):
NOUVEAU = "nouveau"
EN_COURS = "en cours"
CLOTURE = "clôturé"

class Meta:
verbose_name = "Etat"
verbose_name_plural = "Etats"
Expand All @@ -369,7 +373,14 @@ class Meta:

@classmethod
def get_etat_initial(cls):
return cls.objects.get(libelle="nouveau").id
return cls.objects.get(libelle=cls.NOUVEAU).id

@classmethod
def get_etat_cloture(cls):
return cls.objects.get(libelle=cls.CLOTURE)

def is_cloture(self):
return self.libelle == self.CLOTURE

def __str__(self):
return self.libelle
Expand Down Expand Up @@ -449,6 +460,10 @@ def _add_message_url(self, message_type):
"message-add", kwargs={"message_type": message_type, "obj_type_pk": content_type.pk, "obj_pk": self.pk}
)

def cloturer(self):
self.etat = Etat.get_etat_cloture()
self.save()

@property
def add_message_url(self):
return self._add_message_url(Message.MESSAGE)
Expand All @@ -475,3 +490,9 @@ def add_fin_suivi_url(self):

def __str__(self):
return str(self.numero)

def can_be_cloturer_by(self, user):
return user.agent.structure.is_ac

def is_already_cloturer(self):
return self.etat.is_cloture()
5 changes: 4 additions & 1 deletion sv/templates/sv/_action_navigation.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
<div class="fr-collapse fr-translate__menu fr-menu" id="action-1">
<ul class="fr-menu__list">
<li><a class="fr-translate__language fr-nav__link" href="#" data-fr-opened="false" aria-controls="fr-modal-freelink"><span class="fr-icon-git-pull-request-fill fr-mr-2v fr-icon--sm" aria-hidden="true"></span>Ajouter un lien libre</a></li>
{% if user.agent.structure.is_ac and not fichedetection.etat.is_cloture %}
alanzirek marked this conversation as resolved.
Show resolved Hide resolved
<li><a class="fr-translate__language fr-nav__link" href="#" data-fr-opened="false" aria-controls="fr-modal-cloturer-fiche"><span class="fr-icon-close-circle-fill fr-mr-2v fr-icon--sm" aria-hidden="true"></span>Clôturer la fiche</a></li>
{% endif %}
<li><a class="fr-translate__language fr-nav__link" href="#" data-fr-opened="false" aria-controls="fr-modal-delete"><span class="fr-icon-close-circle-fill fr-mr-2v fr-icon--sm" aria-hidden="true"></span>Supprimer la fiche</a></li>
</ul>
</div>
</div>
</nav>
</nav>
41 changes: 41 additions & 0 deletions sv/templates/sv/_fichedetection_cloturer_modal.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<dialog aria-labelledby="fr-modal-cloturer-fiche-title" id="fr-modal-cloturer-fiche" class="fr-modal" role="dialog">
<div class="fr-container fr-container--fluid fr-container-md">
<div class="fr-grid-row fr-grid-row--center">
<div class="fr-col-12 fr-col-md-8 fr-col-lg-6">
<div class="fr-modal__body">
<div class="fr-modal__header">
<button class="fr-btn--close fr-btn" aria-controls="fr-modal-cloturer-fiche">Fermer</button>
</div>
<div class="fr-modal__content">
<h1 id="fr-modal-cloturer-fiche-title" class="fr-modal__title">
<span class="fr-icon-arrow-right-line fr-icon--lg"></span>
Clôturer une fiche
</h1>
{% if can_cloturer_fiche %}
<p>Étes-vous sûr.e de vouloir clôturer la fiche {{ fichedetection.numero }} ?</p>
{% else %}
<p>Vous ne pouvez pas clôturer la fiche n° {{ fichedetection.numero }} car les structures suivantes n'ont pas signalées la fin de suivi : </p>
<ul>
{% for contact in contacts_not_in_fin_suivi %}
<li>{{ contact.structure }}</li>
{% endfor %}
{% endif %}
</div>
{% if can_cloturer_fiche %}
<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-fiche">
Annuler
</button>
<form method="post" action="{% url 'fiche-detection-cloturer' fichedetection.pk %}">
{% csrf_token %}
<button type="submit" class="fr-btn">Confirmer la clôture</button>
</form>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</dialog>
1 change: 1 addition & 0 deletions sv/templates/sv/fichedetection_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ <h1 class="fiche-header__title">Fiche détection n° {{ fichedetection.numero }}
{% include "sv/_action_navigation.html" %}
{% include "sv/_lienlibre_modal.html" %}
{% include "sv/_delete_fiche_modal.html" %}
{% include "sv/_fichedetection_cloturer_modal.html" %}
</div>
</div>

Expand Down
134 changes: 130 additions & 4 deletions sv/tests/test_fichedetection_etats.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
import pytest
from model_bakery import baker
from sv.models import Etat, FicheDetection, Contact
from sv.models import FicheDetection, Etat, Structure
from django.utils.timezone import now, timedelta
from django.core.management import call_command
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


@pytest.fixture
def etat_nouveau(db):
return Etat.objects.get_or_create(libelle="nouveau")[0]
return Etat.objects.get_or_create(libelle=Etat.NOUVEAU)[0]


@pytest.fixture
def create_fiche(db):
return baker.make(FicheDetection, date_creation=now() - timedelta(days=15))


@pytest.fixture
def contact_ac(db):
ac_structure = Structure.objects.create(niveau1=AC_STRUCTURE, niveau2=MUS_STRUCTURE, libelle=MUS_STRUCTURE)
return Contact.objects.create(structure=ac_structure)


def _add_contacts_to_fiche(fiche_detection, mocked_authentification_user):
"""Ajoute l'agent et la structure de l'agent connecté aux contacts de la fiche."""
user_contact_agent = Contact.objects.get(agent=mocked_authentification_user.agent)
user_contact_structure = Contact.objects.get(structure=mocked_authentification_user.agent.structure)
fiche_detection.contacts.add(user_contact_agent)
Expand All @@ -34,7 +44,7 @@ def test_command_updates_fiche_status(create_fiche):
si elles ont été créées il y a plus de 15 jours."""
call_command("update_fichedetection_etat")
fiche = FicheDetection.objects.first()
assert fiche.etat.libelle == "en cours"
assert fiche.etat.libelle == Etat.EN_COURS


def test_element_suivi_fin_suivi_creates_etat_fin_suivi(
Expand All @@ -50,7 +60,7 @@ def test_element_suivi_fin_suivi_creates_etat_fin_suivi(
page.get_by_label("Message :").fill("test")
page.get_by_test_id("fildesuivi-add-submit").click()
page.get_by_test_id("contacts").click()
expect(page.locator("p").filter(has_text="Fin de suivi")).to_be_visible()
expect(page.get_by_label("Contacts").get_by_text("Fin de suivi")).to_be_visible()


def test_element_suivi_fin_suivi_already_exists(
Expand Down Expand Up @@ -88,3 +98,119 @@ def test_cannot_create_fin_suivi_if_structure_not_in_contacts(
)
rows = page.locator("table.fil-de-suivi tbody tr")
expect(rows).to_have_count(0)


def test_can_cloturer_fiche_if_creator_structure_in_fin_suivi(
live_server, page: Page, fiche_detection: FicheDetection, mocked_authentification_user, contact_ac: Contact
):
"""Test qu'un agent de l'AC connecté peut cloturer une fiche détection si la structure du créateur (seule présente dans la liste des contacts) de la fiche est en fin de suivi."""
mocked_authentification_user.agent.structure = contact_ac.structure
fiche_detection.contacts.add(contact_ac)

page.goto(f"{live_server.url}{fiche_detection.get_absolute_url()}")
page.get_by_test_id("element-actions").click()
page.get_by_role("link", name="Signaler la fin de suivi").click()
page.get_by_label("Message :").fill("a")
page.get_by_test_id("fildesuivi-add-submit").click()
page.get_by_test_id("fiche-action").click()
page.get_by_role("link", name="Clôturer la fiche").click()
page.get_by_role("button", name="Confirmer la clôture").click()

expect(page.get_by_text(f"La fiche de détection n° {fiche_detection.numero} a bien été clôturée.")).to_be_visible()
expect(page.get_by_text("clôturé", exact=True)).to_be_visible()
page.get_by_test_id("fiche-action").click()
expect(page.get_by_role("link", name="Clôturer la fiche")).not_to_be_visible()
assert FicheDetection.objects.get(id=fiche_detection.id).etat == Etat.objects.get(libelle=Etat.CLOTURE)


def test_can_cloturer_fiche_if_contacts_structures_in_fin_suivi(
live_server, page: Page, fiche_detection: FicheDetection, mocked_authentification_user, contact_ac: Contact
):
"""Test qu'un agent de l'AC connecté peut cloturer une fiche détection si toutes les structures de la liste des contacts sont en fin de suivi."""
mocked_authentification_user.agent.structure = contact_ac.structure
contact2 = Contact.objects.create(structure=baker.make(Structure, _fill_optional=True))

fiche_detection.contacts.add(contact2)
fiche_detection.contacts.add(contact_ac)

fiche_detection_content_type = ContentType.objects.get_for_model(fiche_detection)
FinSuiviContact.objects.create(
content_type=fiche_detection_content_type, object_id=fiche_detection.id, contact=contact2
)
FinSuiviContact.objects.create(
content_type=fiche_detection_content_type,
object_id=fiche_detection.id,
contact=contact_ac,
)

page.goto(f"{live_server.url}{fiche_detection.get_absolute_url()}")
page.get_by_test_id("fiche-action").click()
page.get_by_role("link", name="Clôturer la fiche").click()
page.get_by_role("button", name="Confirmer la clôture").click()

assert FicheDetection.objects.get(id=fiche_detection.id).etat == Etat.objects.get(libelle=Etat.CLOTURE)


def test_cannot_cloturer_fiche_if_creator_structure_not_in_fin_suivi(
live_server, page: Page, fiche_detection: FicheDetection, mocked_authentification_user, contact_ac: Contact
):
"""Test qu'un agent de l'AC connecté ne peut pas cloturer une fiche détection si la structure du créateur de la fiche n'est pas en fin de suivi."""
mocked_authentification_user.agent.structure = contact_ac.structure
fiche_detection.contacts.add(contact_ac)

page.goto(f"{live_server.url}{fiche_detection.get_absolute_url()}")
page.get_by_test_id("fiche-action").click()
page.get_by_role("link", name="Clôturer la fiche").click()

expect(page.get_by_label("Clôturer une fiche").get_by_role("paragraph")).to_contain_text(
f"Vous ne pouvez pas clôturer la fiche n° {fiche_detection.numero} car les structures suivantes n'ont pas signalées la fin de suivi :"
)
expect(page.get_by_label("Clôturer une fiche").get_by_role("listitem")).to_contain_text(
contact_ac.structure.libelle
)
assert FicheDetection.objects.get(id=fiche_detection.id).etat == Etat.objects.get(libelle=Etat.NOUVEAU)


def test_cannot_cloturer_fiche_if_on_off_contacts_structures_not_in_fin_suivi(
live_server, page: Page, fiche_detection: FicheDetection, mocked_authentification_user, contact_ac: Contact
):
"""Test qu'un agent de l'AC connecté ne peut pas cloturer une fiche détection si une structure de la liste des contacts n'est pas en fin de suivi."""
mocked_authentification_user.agent.structure = contact_ac.structure
contact2 = Contact.objects.create(structure=baker.make(Structure, _fill_optional=True))

fiche_detection.contacts.add(contact2)
fiche_detection.contacts.add(contact_ac)

fiche_detection_content_type = ContentType.objects.get_for_model(fiche_detection)
FinSuiviContact.objects.create(
content_type=fiche_detection_content_type,
object_id=fiche_detection.id,
contact=contact_ac,
)

page.goto(f"{live_server.url}{fiche_detection.get_absolute_url()}")
page.get_by_test_id("fiche-action").click()
page.get_by_role("link", name="Clôturer la fiche").click()

expect(page.get_by_label("Clôturer une fiche").get_by_role("paragraph")).to_contain_text(
f"Vous ne pouvez pas clôturer la fiche n° {fiche_detection.numero} car les structures suivantes n'ont pas signalées la fin de suivi :"
)
expect(page.get_by_label("Clôturer une fiche").get_by_role("listitem")).to_contain_text(contact2.structure.libelle)
assert FicheDetection.objects.get(id=fiche_detection.id).etat == Etat.objects.get(libelle=Etat.NOUVEAU)

alanzirek marked this conversation as resolved.
Show resolved Hide resolved

def test_cannot_cloturer_fiche_if_user_is_not_ac(
live_server, page: Page, fiche_detection: FicheDetection, mocked_authentification_user
):
"""Test qu'un agent connecté non membre de l'AC ne peut pas cloturer une fiche détection même si la/les structure(s) de la liste des contacts sont en fin de suivi."""
contact = Contact.objects.create(structure=baker.make(Structure, _fill_optional=True))
fiche_detection.contacts.add(contact)
FinSuiviContact.objects.create(
content_type=ContentType.objects.get_for_model(fiche_detection), object_id=fiche_detection.id, contact=contact
)

page.goto(f"{live_server.url}{fiche_detection.get_absolute_url()}")
page.get_by_test_id("fiche-action").click()

expect(page.get_by_role("link", name="Clôturer la fiche")).not_to_be_visible()
assert FicheDetection.objects.get(id=fiche_detection.id).etat == Etat.objects.get(libelle=Etat.NOUVEAU)
19 changes: 18 additions & 1 deletion sv/tests/test_fichedetection_performances.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest
from model_bakery import baker

from core.models import Message, Document
from core.models import Message, Document, Structure, Contact
from sv.models import FicheDetection, Lieu, Prelevement

BASE_NUM_QUERIES = 12 # Please note a first call is made without assertion to warm up any possible cache
Expand Down Expand Up @@ -75,3 +75,20 @@ def test_fiche_detection_performances_with_prelevement(client, django_assert_num

with django_assert_num_queries(BASE_NUM_QUERIES):
client.get(fiche.get_absolute_url())


@pytest.mark.django_db
def test_fiche_detection_performances_when_adding_structure(client, django_assert_num_queries):
fiche = baker.make(FicheDetection)
client.get(fiche.get_absolute_url())

with django_assert_num_queries(BASE_NUM_QUERIES):
client.get(fiche.get_absolute_url())

for _ in range(0, 10):
structure = baker.make(Structure)
contact = baker.make(Contact, structure=structure, agent=None)
fiche.contacts.add(contact)

with django_assert_num_queries(BASE_NUM_QUERIES + 1):
client.get(fiche.get_absolute_url())
10 changes: 6 additions & 4 deletions sv/tests/test_fichedetection_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -673,12 +673,14 @@ def test_update_multiple_prelevements(
page.locator("#fiche-detection-form #prelevements").get_by_role("button").nth(3).click()

prelevement_form_elements.lieu_input.select_option(str(new_prelevement.lieu.id))
prelevement_form_elements.structure_input.select_option(str(new_prelevement.structure_preleveur.id))
prelevement_form_elements.structure_input.select_option(value=str(new_prelevement.structure_preleveur.id))
prelevement_form_elements.numero_echantillon_input.fill(new_prelevement.numero_echantillon)
prelevement_form_elements.date_prelevement_input.fill(new_prelevement.date_prelevement.strftime("%Y-%m-%d"))
prelevement_form_elements.site_inspection_input.select_option(str(new_prelevement.site_inspection.id))
prelevement_form_elements.matrice_prelevee_input.select_option(str(new_prelevement.matrice_prelevee.id))
prelevement_form_elements.espece_echantillon_input.select_option(str(new_prelevement.espece_echantillon.id))
prelevement_form_elements.site_inspection_input.select_option(value=str(new_prelevement.site_inspection.id))
prelevement_form_elements.matrice_prelevee_input.select_option(value=str(new_prelevement.matrice_prelevee.id))
prelevement_form_elements.espece_echantillon_input.select_option(
value=str(new_prelevement.espece_echantillon.id)
)
prelevement_form_elements.resultat_input(new_prelevement.resultat).click()
prelevement_form_elements.save_btn.click()

Expand Down
6 changes: 3 additions & 3 deletions sv/tests/test_structure_add.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def test_add_structure_to_a_fiche(live_server, page, fiche_detection, contacts_s
page.get_by_text(contacts_structure[0].structure.libelle).click()
page.get_by_role("button", name="Ajouter les structures sélectionnées").click()
expect(page.get_by_text("La structure a été ajoutée avec succès.")).to_be_visible()
expect(page.get_by_text(contacts_structure[0].structure.libelle, exact=True)).to_be_visible()
expect(page.locator("p").filter(has_text=contacts_structure[0].structure.libelle)).to_be_visible()


@pytest.mark.django_db
Expand All @@ -81,8 +81,8 @@ def test_add_multiple_structures_to_a_fiche(live_server, page, fiche_detection):
page.get_by_role("button", name="Ajouter les structures sélectionnées").click()

expect(page.get_by_text("Les 2 structures ont été ajoutées avec succès.")).to_be_visible()
expect(page.get_by_text(contact1.structure.libelle, exact=True)).to_be_visible()
expect(page.get_by_text(contact2.structure.libelle, exact=True)).to_be_visible()
expect(page.locator(".bloc-commun__panel--contacts")).to_contain_text(contact1.structure.libelle)
expect(page.locator(".bloc-commun__panel--contacts")).to_contain_text(contact2.structure.libelle)


@pytest.mark.django_db
Expand Down
Loading