Add audit functionality for Comment actions and integrate CommentSubjectConverter and CommentSubjectDisplayer

- Introduced `CommentSubjectConverter` and `CommentSubjectDisplayer` for audit conversion and display logic.
- Integrated `TriggerAuditInterface` into `AccompanyingCourseCommentController` and added audit triggers for create, view, update, delete, pin, unpin, and list actions with translatable descriptions.
- Added unit test `CommentSubjectConverterTest` to ensure proper behavior of the subject converter.
- Enhanced French translations (`messages.fr.yml`) with audit-related labels for comment actions.
This commit is contained in:
2026-03-02 10:30:58 +01:00
parent 56069deaf5
commit 222d20734c
5 changed files with 186 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Audit\Displayer;
use Chill\MainBundle\Audit\Subject;
use Chill\MainBundle\Audit\SubjectDisplayerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
final readonly class CommentSubjectDisplayer implements SubjectDisplayerInterface
{
public function __construct(
private TranslatorInterface $translator,
) {}
public function supportsDisplay(Subject $subject, array $options = []): bool
{
return 'accompanying_period_comment' === $subject->type;
}
public function display(Subject $subject, string $format = 'html', array $options = []): string
{
return $this->translator->trans('audit.accompanying_period_comment.comment_number', ['{id}' => $subject->identifiers['id']]);
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Audit\SubjectConverter;
use Chill\MainBundle\Audit\Subject;
use Chill\MainBundle\Audit\SubjectBag;
use Chill\MainBundle\Audit\SubjectConverterInterface;
use Chill\MainBundle\Audit\SubjectConverterManagerAwareInterface;
use Chill\MainBundle\Audit\SubjectConverterManagerAwareTrait;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment;
/**
* @implements SubjectConverterInterface<Comment>
*/
class CommentSubjectConverter implements SubjectConverterInterface, SubjectConverterManagerAwareInterface
{
use SubjectConverterManagerAwareTrait;
public function convert(mixed $subject, bool $includeAssociated = false): SubjectBag
{
\assert($subject instanceof Comment);
$main = new SubjectBag(new Subject('accompanying_period_comment', ['id' => $subject->getId()]));
if (null !== $subject->getAccompanyingPeriod()) {
$main->append($this->subjectConverterManager->getSubjectsForEntity($subject->getAccompanyingPeriod(), false));
}
return $main;
}
public function supportsConvert(mixed $subject): bool
{
return $subject instanceof Comment;
}
public static function getDefaultPriority(): int
{
return 0;
}
}

View File

@@ -11,6 +11,8 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\Audit\TriggerAuditInterface;
use Chill\MainBundle\Entity\AuditTrail;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Form\AccompanyingCourseCommentType;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodCommentVoter;
@@ -27,6 +29,7 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Contracts\Translation\TranslatorInterface;
class AccompanyingCourseCommentController extends AbstractController
@@ -36,6 +39,7 @@ class AccompanyingCourseCommentController extends AbstractController
private readonly FormFactoryInterface $formFactory,
private readonly TranslatorInterface $translator,
private readonly ManagerRegistry $managerRegistry,
private readonly TriggerAuditInterface $triggerAudit,
) {}
/**
@@ -48,6 +52,12 @@ class AccompanyingCourseCommentController extends AbstractController
{
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE_DETAILS, $accompanyingCourse);
$this->triggerAudit->triggerAudit(
AuditTrail::AUDIT_LIST,
$accompanyingCourse,
description: new TranslatableMessage('audit.accompanying_period_comment.list')
);
$em = $this->managerRegistry->getManager();
$afterSuccessfulRedirectResponse = $this->redirectToRoute('chill_person_accompanying_period_comment_list', [
'accompanying_period_id' => $accompanyingCourse->getId(),
@@ -84,6 +94,8 @@ class AccompanyingCourseCommentController extends AbstractController
if ($editForm->isSubmitted() && $editForm->isValid()) {
$em->flush();
$this->triggerAudit->triggerAudit(AuditTrail::AUDIT_UPDATE, $commentEdited);
return $afterSuccessfulRedirectResponse;
}
@@ -103,6 +115,8 @@ class AccompanyingCourseCommentController extends AbstractController
$em->persist($newComment);
$em->flush();
$this->triggerAudit->triggerAudit(AuditTrail::AUDIT_CREATE, $newComment);
return $afterSuccessfulRedirectResponse;
}
}
@@ -129,6 +143,8 @@ class AccompanyingCourseCommentController extends AbstractController
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->triggerAudit->triggerAudit(AuditTrail::AUDIT_DELETE, $comment);
$this->entityManager->remove($comment);
if ($comment->getAccompanyingPeriod()->getPinnedComment() === $comment) {
@@ -163,6 +179,12 @@ class AccompanyingCourseCommentController extends AbstractController
$this->managerRegistry->getManager()->flush();
$this->triggerAudit->triggerAudit(
AuditTrail::AUDIT_UPDATE,
$comment,
description: new TranslatableMessage('audit.accompanying_period_comment.pin')
);
$this->addFlash('success', $this->translator->trans('accompanying_course.comment is pinned'));
return $this->redirectToRoute('chill_person_accompanying_period_comment_list', [
@@ -179,6 +201,12 @@ class AccompanyingCourseCommentController extends AbstractController
$this->managerRegistry->getManager()->flush();
$this->triggerAudit->triggerAudit(
AuditTrail::AUDIT_UPDATE,
$comment,
description: new TranslatableMessage('audit.accompanying_period_comment.unpin')
);
$this->addFlash('success', $this->translator->trans('accompanying_course.comment is unpinned'));
return $this->redirectToRoute('chill_person_accompanying_period_comment_list', [

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Tests\Audit\SubjectConverter;
use Chill\MainBundle\Audit\Subject;
use Chill\MainBundle\Audit\SubjectBag;
use Chill\MainBundle\Audit\SubjectConverterManagerInterface;
use Chill\PersonBundle\Audit\SubjectConverter\CommentSubjectConverter;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
/**
* @internal
*
* @covers \Chill\PersonBundle\Audit\SubjectConverter\CommentSubjectConverter
*/
class CommentSubjectConverterTest extends TestCase
{
use ProphecyTrait;
private CommentSubjectConverter $converter;
protected function setUp(): void
{
$this->converter = new CommentSubjectConverter();
}
public function testSupportsConvert(): void
{
$this->assertTrue($this->converter->supportsConvert($this->prophesize(Comment::class)->reveal()));
$this->assertFalse($this->converter->supportsConvert(new \stdClass()));
}
public function testConvert(): void
{
$comment = $this->prophesize(Comment::class);
$comment->getId()->willReturn(456);
$accompanyingPeriod = $this->prophesize(AccompanyingPeriod::class);
$comment->getAccompanyingPeriod()->willReturn($accompanyingPeriod->reveal());
$subjectConverterManager = $this->prophesize(SubjectConverterManagerInterface::class);
$accompanyingPeriodSubject = new Subject('accompanying_period', ['id' => 123]);
$accompanyingPeriodSubjectBag = new SubjectBag($accompanyingPeriodSubject);
$subjectConverterManager->getSubjectsForEntity($accompanyingPeriod->reveal(), false)
->willReturn($accompanyingPeriodSubjectBag);
$this->converter->setSubjectConverterManager($subjectConverterManager->reveal());
$result = $this->converter->convert($comment->reveal());
$this->assertSame('accompanying_period_comment', $result->subject->type);
$this->assertSame(['id' => 456], $result->subject->identifiers);
$this->assertCount(1, $result->associatedSubjects);
$this->assertSame($accompanyingPeriodSubject, $result->associatedSubjects[0]);
}
}

View File

@@ -1608,3 +1608,8 @@ audit:
household_member:
not_found_with_id: 'Membre du ménage non trouvé avec identifiant {id}'
member_in_household: 'membre du ménage'
accompanying_period_comment:
comment_number: "Commentaire n°{id}"
list: "Liste des commentaires d'un parcours"
pin: "Commentaire épinglé"
unpin: "Commentaire désépinglé"