From d2f5b49131bcddb92aff4a8056b41e41c78b5ced Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 28 Jan 2026 17:32:27 +0100 Subject: [PATCH] Add audit logging and subject conversion for accompanying periods - Added `AccompanyingPeriodSubjectConverter` to handle subject conversion for accompanying periods. - Integrated `AuditEvent` dispatching into `AccompanyingCourseController` for key actions (e.g., create, view, update, delete, reopen). - Extended translations to include audit-related messages for accompanying periods. - Introduced tests for `AccompanyingPeriodSubjectConverter` to ensure proper functionality. --- .../AccompanyingPeriodSubjectConverter.php | 54 ++++++++++++ .../AccompanyingCourseController.php | 31 +++++++ ...AccompanyingPeriodSubjectConverterTest.php | 84 +++++++++++++++++++ .../translations/messages.fr.yml | 5 ++ 4 files changed, 174 insertions(+) create mode 100644 src/Bundle/ChillPersonBundle/Audit/SubjectConverter/AccompanyingPeriodSubjectConverter.php create mode 100644 src/Bundle/ChillPersonBundle/Tests/Audit/SubjectConverter/AccompanyingPeriodSubjectConverterTest.php diff --git a/src/Bundle/ChillPersonBundle/Audit/SubjectConverter/AccompanyingPeriodSubjectConverter.php b/src/Bundle/ChillPersonBundle/Audit/SubjectConverter/AccompanyingPeriodSubjectConverter.php new file mode 100644 index 000000000..2320f6d3e --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Audit/SubjectConverter/AccompanyingPeriodSubjectConverter.php @@ -0,0 +1,54 @@ + + */ +class AccompanyingPeriodSubjectConverter implements SubjectConverterInterface, SubjectConverterManagerAwareInterface +{ + use SubjectConverterManagerAwareTrait; + + public function convert(mixed $subject): array + { + $data = [new Subject('accompanying_period', ['id' => $subject->getId()])]; + + foreach ($subject->getCurrentParticipations() as $participation) { + $subjects = $this->subjectConverterManager->getSubjectsForEntity($participation->getPerson()); + if ($subjects instanceof Subject) { + $data[] = $subjects; + } else { + foreach ($subjects as $s) { + $data[] = $s; + } + } + } + + return $data; + } + + public function supportsConvert(mixed $subject): bool + { + return $subject instanceof AccompanyingPeriod; + } + + public static function getDefaultPriority(): int + { + return 10; + } +} diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseController.php index 79b0b8046..57432e280 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseController.php @@ -12,6 +12,8 @@ declare(strict_types=1); namespace Chill\PersonBundle\Controller; use Chill\ActivityBundle\Entity\Activity; +use Chill\MainBundle\Audit\AuditEvent; +use Chill\MainBundle\Entity\AuditTrail; use Chill\MainBundle\Entity\User; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Household\Household; @@ -20,6 +22,7 @@ use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepos use Chill\PersonBundle\Repository\PersonRepository; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; use Chill\PersonBundle\Security\Authorization\PersonVoter; +use Psr\EventDispatcher\EventDispatcherInterface; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\HttpFoundation\Request; @@ -27,6 +30,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Security; +use Symfony\Component\Translation\TranslatableMessage; use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\ConstraintViolationListInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -46,6 +50,7 @@ final class AccompanyingCourseController extends \Symfony\Bundle\FrameworkBundle private readonly Security $security, private readonly PersonRepository $personRepository, private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry, + private readonly EventDispatcherInterface $eventDispatcher, ) {} /** @@ -72,6 +77,15 @@ final class AccompanyingCourseController extends \Symfony\Bundle\FrameworkBundle $em->flush(); + $this->eventDispatcher->dispatch( + new AuditEvent( + AuditTrail::AUDIT_UPDATE, + [$accompanyingCourse], + new TranslatableMessage('accompanying_period.audit.close'), + ['action' => 'close'] + ) + ); + return $this->redirectToRoute('chill_person_accompanying_course_index', [ 'accompanying_period_id' => $accompanyingCourse->getId(), ]); @@ -119,6 +133,8 @@ final class AccompanyingCourseController extends \Symfony\Bundle\FrameworkBundle $em->remove($accompanyingCourse); $em->flush(); + $this->eventDispatcher->dispatch(new AuditEvent(AuditTrail::AUDIT_DELETE, [$accompanyingCourse])); + $this->addFlash('success', $this->translator ->trans('The accompanying course has been successfully removed.')); @@ -157,6 +173,13 @@ final class AccompanyingCourseController extends \Symfony\Bundle\FrameworkBundle { $this->denyAccessUnlessGranted(AccompanyingPeriodVoter::EDIT, $accompanyingCourse); + $this->eventDispatcher->dispatch(new AuditEvent( + AuditTrail::AUDIT_VIEW, + [$accompanyingCourse], + new TranslatableMessage('accompanying_period.audit.show_edit_page'), + ['action' => 'show_edit_page'] + )); + return $this->render('@ChillPerson/AccompanyingCourse/edit.html.twig', [ 'accompanyingCourse' => $accompanyingCourse, ]); @@ -189,6 +212,8 @@ final class AccompanyingCourseController extends \Symfony\Bundle\FrameworkBundle { $this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $accompanyingCourse); + $this->eventDispatcher->dispatch(new AuditEvent(AuditTrail::AUDIT_VIEW, [$accompanyingCourse])); + // compute some warnings // get persons without household $withoutHousehold = []; @@ -258,6 +283,8 @@ final class AccompanyingCourseController extends \Symfony\Bundle\FrameworkBundle $em->persist($period); $em->flush(); + $this->eventDispatcher->dispatch(new AuditEvent(AuditTrail::AUDIT_CREATE, [$period])); + return $this->redirectToRoute('chill_person_accompanying_course_edit', [ 'accompanying_period_id' => $period->getId(), ]); @@ -295,6 +322,8 @@ final class AccompanyingCourseController extends \Symfony\Bundle\FrameworkBundle $em->persist($period); $em->flush(); + $this->eventDispatcher->dispatch(new AuditEvent(AuditTrail::AUDIT_CREATE, [$period])); + return $this->redirectToRoute('chill_person_accompanying_course_edit', [ 'accompanying_period_id' => $period->getId(), ]); @@ -319,6 +348,8 @@ final class AccompanyingCourseController extends \Symfony\Bundle\FrameworkBundle $accompanyingCourse->reOpen(); $this->managerRegistry->getManager()->flush(); + $this->eventDispatcher->dispatch(new AuditEvent(AuditTrail::AUDIT_UPDATE, [$accompanyingCourse], new TranslatableMessage('accompanying_period.audit.reopen'), ['action' => 'reopen'])); + return $this->redirectToRoute('chill_person_accompanying_course_index', [ 'accompanying_period_id' => $accompanyingCourse->getId(), ]); diff --git a/src/Bundle/ChillPersonBundle/Tests/Audit/SubjectConverter/AccompanyingPeriodSubjectConverterTest.php b/src/Bundle/ChillPersonBundle/Tests/Audit/SubjectConverter/AccompanyingPeriodSubjectConverterTest.php new file mode 100644 index 000000000..f2bee8c32 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Audit/SubjectConverter/AccompanyingPeriodSubjectConverterTest.php @@ -0,0 +1,84 @@ +converter = new AccompanyingPeriodSubjectConverter(); + } + + public function testSupportsConvert(): void + { + $this->assertTrue($this->converter->supportsConvert($this->prophesize(AccompanyingPeriod::class)->reveal())); + $this->assertFalse($this->converter->supportsConvert(new \stdClass())); + } + + public function testConvert(): void + { + $accompanyingPeriod = $this->prophesize(AccompanyingPeriod::class); + $accompanyingPeriod->getId()->willReturn(123); + + $person1 = $this->prophesize(Person::class); + $participation1 = $this->prophesize(AccompanyingPeriodParticipation::class); + $participation1->getPerson()->willReturn($person1->reveal()); + + $person2 = $this->prophesize(Person::class); + $participation2 = $this->prophesize(AccompanyingPeriodParticipation::class); + $participation2->getPerson()->willReturn($person2->reveal()); + + $accompanyingPeriod->getCurrentParticipations()->willReturn(new ArrayCollection([ + $participation1->reveal(), + $participation2->reveal(), + ])); + + $subjectConverterManager = $this->prophesize(SubjectConverterManagerInterface::class); + + $personSubject1 = new Subject('person', ['id' => 1]); + $personSubject2 = new Subject('person', ['id' => 2]); + + $subjectConverterManager->getSubjectsForEntity($person1->reveal())->willReturn($personSubject1); + $subjectConverterManager->getSubjectsForEntity($person2->reveal())->willReturn([$personSubject2]); + + $this->converter->setSubjectConverterManager($subjectConverterManager->reveal()); + + $result = $this->converter->convert($accompanyingPeriod->reveal()); + + $this->assertCount(3, $result); + $this->assertInstanceOf(Subject::class, $result[0]); + $this->assertSame('accompanying_period', $result[0]->type); + $this->assertSame(['id' => 123], $result[0]->identifiers); + + $this->assertSame($personSubject1, $result[1]); + $this->assertSame($personSubject2, $result[2]); + } +} diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index b62908dc0..d35096199 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -861,6 +861,11 @@ accompanying_period: CONFIRMED_INACTIVE_SHORT: Hors file active CONFIRMED_INACTIVE_LONG: Pré-archivé emergency: Urgent + audit: + reopen: 'Ré-ouverture du parcours' + close: 'Clôture du parcours' + show_edit_page: 'Visualiser la page de modification du parcours' + show_list_work: 'Liste des actions d''accompagnement' occasional: ponctuel regular: régulier Confidential: confidentiel