From e28dc355e8cbbc69f72394647d36d5d8bf5d4c50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 27 Feb 2026 16:13:37 +0100 Subject: [PATCH] Add audit functionality for `Activity` actions - Introduced `ActivityDisplayer` and `ActivitySubjectConverter` for handling audit display and conversion logic. - Integrated `TriggerAuditInterface` in `ActivityController` and added audit triggers for create, list, update, delete, and view actions with translatable descriptions. - Updated `services.yaml` to register new audit-related services. - Enhanced French translations with audit-related labels for `Activity`. --- docs/source/development/audit.md | 3 +- .../Converter/ActivitySubjectConverter.php | 56 +++++++++++++++++++ .../Audit/Displayer/ActivityDisplayer.php | 49 ++++++++++++++++ .../Controller/ActivityController.php | 17 ++++++ .../ChillActivityBundle/config/services.yaml | 3 + .../translations/messages+intl-icu.fr.yml | 7 +++ 6 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 src/Bundle/ChillActivityBundle/Audit/Converter/ActivitySubjectConverter.php create mode 100644 src/Bundle/ChillActivityBundle/Audit/Displayer/ActivityDisplayer.php diff --git a/docs/source/development/audit.md b/docs/source/development/audit.md index 4a09bfa06..31c71f7c9 100644 --- a/docs/source/development/audit.md +++ b/docs/source/development/audit.md @@ -68,6 +68,7 @@ $this->triggerAudit->triggerAudit( - Audit every create, update, delete, view and list action on domain entities. - Use only the `$action` when it is obvious and self-explanatory. - Use the `$description` when the action alone is not enough. For example for a list, the main subject is the person (or the accompanying period) the list is attached to, and the description should summarize the list content (filters, ranges, counts, …). +- The description should be a `TranslatableMessage`, and the first key of the message should be `audit` - Add `$metadata` to provide extra, machine-friendly details (ids, filters, pagination, counts, etc.). - Reuse the existing patterns found in current controllers and services. @@ -84,7 +85,7 @@ Examples: ($this->triggerAudit)( AuditTrail::AUDIT_LIST, $accompanyingPeriod, - description: 'Listing resources for the current period', + description: new TranslatableMessage('audit.Listing resources for the current period'), metadata: ['scope' => 'resources', 'filters' => ['category' => 'housing', 'page' => 1]] ); ``` diff --git a/src/Bundle/ChillActivityBundle/Audit/Converter/ActivitySubjectConverter.php b/src/Bundle/ChillActivityBundle/Audit/Converter/ActivitySubjectConverter.php new file mode 100644 index 000000000..3e275af6c --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Audit/Converter/ActivitySubjectConverter.php @@ -0,0 +1,56 @@ + $subject->getId()], + ); + + $bag = new SubjectBag($main); + if ($includeAssociated) { + if (null !== $subject->getPerson()) { + $bag->append($this->subjectConverterManager->getSubjectsForEntity($subject->getPerson(), false)); + } + if (null !== $subject->getAccompanyingPeriod()) { + $bag->append($this->subjectConverterManager->getSubjectsForEntity($subject->getAccompanyingPeriod(), false)); + } + } + + return $bag; + } + + public static function getDefaultPriority(): int + { + return 0; + } +} diff --git a/src/Bundle/ChillActivityBundle/Audit/Displayer/ActivityDisplayer.php b/src/Bundle/ChillActivityBundle/Audit/Displayer/ActivityDisplayer.php new file mode 100644 index 000000000..6a594ebf1 --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Audit/Displayer/ActivityDisplayer.php @@ -0,0 +1,49 @@ +type; + } + + public function display(Subject $subject, string $format = 'html', array $options = []): string + { + $activity = $this->activityRepository->find($subject->identifiers['id']); + + if (null === $activity) { + $label = $this->translator->trans('audit.activity.subject', ['id' => $subject->identifiers['id']], 'messages'); + } else { + $label = $this->translator->trans('audit.activity.subject_with_details', [ + 'id' => $subject->identifiers['id'], + 'date' => $activity->getDate(), + 'type' => $this->translatableStringHelper->localize($activity->getActivityType()->getName()), + ], 'messages'); + } + + return 'html' === $format ? ''.$label.'' : $label; + } +} diff --git a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php index bc9adc698..4a0d5b624 100644 --- a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php +++ b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php @@ -19,6 +19,8 @@ use Chill\ActivityBundle\Repository\ActivityRepository; use Chill\ActivityBundle\Repository\ActivityTypeCategoryRepository; use Chill\ActivityBundle\Repository\ActivityTypeRepositoryInterface; use Chill\ActivityBundle\Security\Authorization\ActivityVoter; +use Chill\MainBundle\Audit\TriggerAuditInterface; +use Chill\MainBundle\Entity\AuditTrail; use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable; use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Pagination\PaginatorFactory; @@ -45,6 +47,7 @@ use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Serializer\SerializerInterface; +use Symfony\Component\Translation\TranslatableMessage; use Symfony\Contracts\Translation\TranslatorInterface; final class ActivityController extends AbstractController @@ -69,6 +72,7 @@ final class ActivityController extends AbstractController private readonly TranslatableStringHelperInterface $translatableStringHelper, private readonly PaginatorFactory $paginatorFactory, private readonly ChillSecurity $security, + private readonly TriggerAuditInterface $triggerAudit, ) {} /** @@ -120,6 +124,8 @@ final class ActivityController extends AbstractController 'attendee' => $activity->getAttendee(), ]); + $this->triggerAudit->triggerAudit(AuditTrail::AUDIT_DELETE, $activity); + $this->entityManager->remove($activity); $this->entityManager->flush(); @@ -190,6 +196,8 @@ final class ActivityController extends AbstractController $this->entityManager->persist($entity); $this->entityManager->flush(); + ($this->triggerAudit)(AuditTrail::AUDIT_UPDATE, $entity); + $params = $this->buildParamsToUrl($person, $accompanyingPeriod); $params['id'] = $entity->getId(); @@ -256,6 +264,9 @@ final class ActivityController extends AbstractController if ($person instanceof Person) { $this->denyAccessUnlessGranted(ActivityVoter::SEE, $person); + + ($this->triggerAudit)(AuditTrail::AUDIT_LIST, $person, description: new TranslatableMessage('audit.activity.list_for_person')); + $count = $this->activityACLAwareRepository->countByPerson($person, ActivityVoter::SEE, $filterArgs); $paginator = $this->paginatorFactory->create($count); $activities = $this->activityACLAwareRepository @@ -278,6 +289,8 @@ final class ActivityController extends AbstractController } elseif ($accompanyingPeriod instanceof AccompanyingPeriod) { $this->denyAccessUnlessGranted(ActivityVoter::SEE, $accompanyingPeriod); + ($this->triggerAudit)(AuditTrail::AUDIT_LIST, $accompanyingPeriod, description: new TranslatableMessage('audit.activity.list_for_accompanying_period')); + $count = $this->activityACLAwareRepository->countByAccompanyingPeriod($accompanyingPeriod, ActivityVoter::SEE, $filterArgs); $paginator = $this->paginatorFactory->create($count); $activities = $this->activityACLAwareRepository @@ -482,6 +495,8 @@ final class ActivityController extends AbstractController $this->entityManager->persist($entity); $this->entityManager->flush(); + ($this->triggerAudit)(AuditTrail::AUDIT_CREATE, $entity); + if ($form->has('gendocTemplateId') && null !== $form['gendocTemplateId']->getData()) { return $this->redirectToRoute( 'chill_docgenerator_generate_from_template', @@ -596,6 +611,8 @@ final class ActivityController extends AbstractController $this->denyAccessUnlessGranted(ActivityVoter::SEE, $entity); + ($this->triggerAudit)(AuditTrail::AUDIT_VIEW, $entity); + $deleteForm = $this->createDeleteForm($entity->getId(), $person, $accompanyingPeriod); // TODO diff --git a/src/Bundle/ChillActivityBundle/config/services.yaml b/src/Bundle/ChillActivityBundle/config/services.yaml index 18be76ec9..68759b98c 100644 --- a/src/Bundle/ChillActivityBundle/config/services.yaml +++ b/src/Bundle/ChillActivityBundle/config/services.yaml @@ -39,5 +39,8 @@ services: Chill\ActivityBundle\Service\EntityInfo\: resource: '../Service/EntityInfo/' + Chill\ActivityBundle\Audit\: + resource: '../Audit/' + Chill\ActivityBundle\Service\GenericDoc\: resource: '../Service/GenericDoc/' diff --git a/src/Bundle/ChillActivityBundle/translations/messages+intl-icu.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages+intl-icu.fr.yml index e05a7efd2..4a93560f4 100644 --- a/src/Bundle/ChillActivityBundle/translations/messages+intl-icu.fr.yml +++ b/src/Bundle/ChillActivityBundle/translations/messages+intl-icu.fr.yml @@ -16,3 +16,10 @@ export: activity: title: Échange du {date, date, long} - {type} + +audit: + activity: + subject: "Échange n° {id}" + subject_with_details: "Échange n° {id} du {date, date, short} ({type})" + list_for_person: "Liste des échanges d'un usager" + list_for_accompanying_period: "Liste des échanges d'un parcours"