Add audit functionality for SingleTask and integrate subject converter and displayer

- Introduced `SingleTaskSubjectConverter` for audit conversion logic and `SingleTaskSubjectDisplayer` for display logic, including a Twig template.
- Integrated `TriggerAuditInterface` into `SingleTaskController` and added audit triggers for create, view, update, delete, and list actions with translatable descriptions.
- Registered new audit-related services in `services.yaml` and loaded `services.yaml` in the extension.
- Created unit tests to validate the behavior of the subject converter and related functionality.
- Added French translations for audit messages related to `SingleTask`.
This commit is contained in:
2026-03-02 15:35:52 +01:00
parent 9da82878af
commit 2313b053d4
8 changed files with 241 additions and 1 deletions

View File

@@ -0,0 +1,44 @@
<?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\TaskBundle\Audit\Displayer;
use Chill\MainBundle\Audit\Subject;
use Chill\MainBundle\Audit\SubjectDisplayerInterface;
use Chill\TaskBundle\Repository\SingleTaskRepository;
use Twig\Environment;
final readonly class SingleTaskSubjectDisplayer implements SubjectDisplayerInterface
{
public function __construct(
private SingleTaskRepository $singleTaskRepository,
private Environment $twig,
) {}
public function supportsDisplay(Subject $subject, array $options = []): bool
{
return 'single_task' === $subject->type;
}
public function display(Subject $subject, string $format = 'html', array $options = []): string
{
$task = $this->singleTaskRepository->find($subject->identifiers['id']);
if ('html' === $format) {
return $this->twig->render('@ChillTask/Audit/single_task.html.twig', [
'id' => $subject->identifiers['id'],
'task' => $task,
]);
}
return $task ? sprintf('%s (#%d)', $task->getTitle(), $task->getId()) : sprintf('Single task #%d', $subject->identifiers['id']);
}
}

View File

@@ -0,0 +1,53 @@
<?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\TaskBundle\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\TaskBundle\Entity\SingleTask;
/**
* @implements SubjectConverterInterface<SingleTask>
*/
final class SingleTaskSubjectConverter implements SubjectConverterInterface, SubjectConverterManagerAwareInterface
{
use SubjectConverterManagerAwareTrait;
public function convert(mixed $subject, bool $includeAssociated = false): SubjectBag
{
$mainSubject = new Subject('single_task', ['id' => $subject->getId()]);
$main = new SubjectBag($mainSubject);
if ($includeAssociated) {
if ($subject->getPerson()) {
$main->append($this->subjectConverterManager->getSubjectsForEntity($subject->getPerson(), false));
} elseif ($subject->getCourse()) {
$main->append($this->subjectConverterManager->getSubjectsForEntity($subject->getCourse(), false));
}
}
return $main;
}
public function supportsConvert(mixed $subject): bool
{
return $subject instanceof SingleTask;
}
public static function getDefaultPriority(): int
{
return 0;
}
}

View File

@@ -11,6 +11,8 @@ declare(strict_types=1);
namespace Chill\TaskBundle\Controller;
use Chill\MainBundle\Audit\TriggerAuditInterface;
use Chill\MainBundle\Entity\AuditTrail;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface;
@@ -60,6 +62,7 @@ final class SingleTaskController extends AbstractController
private readonly SingleTaskRepository $singleTaskRepository,
private readonly Security $security,
private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
private readonly TriggerAuditInterface $triggerAudit,
) {}
#[Route(path: '/{_locale}/task/single-task/{id}/delete', name: 'chill_task_single_task_delete', methods: ['GET', 'POST', 'DELETE'])]
@@ -113,6 +116,8 @@ final class SingleTaskController extends AbstractController
if (Request::METHOD_POST === $request->getMethod()) {
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
($this->triggerAudit)(AuditTrail::AUDIT_DELETE, $task);
$this->logger->notice('A task has been removed', [
'by_user' => $this->getUser()->getUsername(),
'task_id' => $task->getId(),
@@ -180,6 +185,8 @@ final class SingleTaskController extends AbstractController
$em->flush();
($this->triggerAudit)(AuditTrail::AUDIT_UPDATE, $task);
$this->addFlash('success', $this->translator
->trans('The task has been updated'));
@@ -309,6 +316,12 @@ final class SingleTaskController extends AbstractController
): Response {
$this->denyAccessUnlessGranted(TaskVoter::SHOW, $course);
($this->triggerAudit)(
AuditTrail::AUDIT_LIST,
$course,
description: new \Symfony\Component\Translation\TranslatableMessage('audit.single_task.list_for_course')
);
$filterOrder = $this->buildFilterOrder();
$flags = \array_merge(
$filterOrder->getCheckboxData('status'),
@@ -354,6 +367,8 @@ final class SingleTaskController extends AbstractController
): Response {
$this->denyAccessUnlessGranted(TaskVoter::SHOW, $person);
($this->triggerAudit)(AuditTrail::AUDIT_LIST, $person, description: new \Symfony\Component\Translation\TranslatableMessage('audit.single_task.list_for_person'));
$filterOrder = $this->buildFilterOrder();
$flags = \array_merge(
$filterOrder->getCheckboxData('status'),
@@ -527,6 +542,8 @@ final class SingleTaskController extends AbstractController
$em->flush();
($this->triggerAudit)(AuditTrail::AUDIT_CREATE, $task);
$this->addFlash('success', $this->translator->trans('The task is created'));
if ($request->query->has('returnPath')) {
@@ -569,6 +586,8 @@ final class SingleTaskController extends AbstractController
{
$this->denyAccessUnlessGranted(TaskVoter::SHOW, $task);
($this->triggerAudit)(AuditTrail::AUDIT_VIEW, $task);
if ($task->getContext() instanceof Person) {
$event = new PrivacyEvent($task->getContext(), [
'element_class' => SingleTask::class,

View File

@@ -42,6 +42,7 @@ class ChillTaskExtension extends Extension implements PrependExtensionInterface
$loader->load('services/timeline.yaml');
$loader->load('services/fixtures.yaml');
$loader->load('services/form.yaml');
$loader->load('services.yaml');
}
public function prepend(ContainerBuilder $container)

View File

@@ -0,0 +1,13 @@
<span>
{% if task is not null %}
<span>
{{ 'audit.single_task.display'|trans({'{id}': id} ) }} -
{{ task.title }}
<a href="{{ path('chill_task_single_task_show', {'id': id}) }}">
<i class="bi bi-link"></i>
</a>
</span>
{% else %}
{{ 'audit.single_task.not_found_with_id'|trans({'{id}': id}, 'messages') }}
{% endif %}
</span>

View File

@@ -0,0 +1,99 @@
<?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\TaskBundle\Tests\Audit\SubjectConverter;
use Chill\MainBundle\Audit\SubjectConverterManagerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\TaskBundle\Audit\SubjectConverter\SingleTaskSubjectConverter;
use Chill\TaskBundle\Entity\SingleTask;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
/**
* @internal
*
* @coversNothing
*/
final class SingleTaskSubjectConverterTest extends TestCase
{
use ProphecyTrait;
public function testSupportsConvert(): void
{
$converter = new SingleTaskSubjectConverter();
$this->assertTrue($converter->supportsConvert($this->createMock(SingleTask::class)));
$this->assertFalse($converter->supportsConvert(new \stdClass()));
}
public function testConvert(): void
{
$converter = new SingleTaskSubjectConverter();
$manager = $this->prophesize(SubjectConverterManagerInterface::class);
$converter->setSubjectConverterManager($manager->reveal());
$task = $this->prophesize(SingleTask::class);
$task->getId()->willReturn(123);
$task->getPerson()->willReturn(null);
$task->getCourse()->willReturn(null);
$bag = $converter->convert($task->reveal(), true);
$this->assertSame('single_task', $bag->subject->type);
$this->assertSame(['id' => 123], $bag->subject->identifiers);
$this->assertCount(0, $bag->associatedSubjects);
}
public function testConvertWithPerson(): void
{
$converter = new SingleTaskSubjectConverter();
$manager = $this->prophesize(SubjectConverterManagerInterface::class);
$converter->setSubjectConverterManager($manager->reveal());
$person = $this->prophesize(Person::class)->reveal();
$task = $this->prophesize(SingleTask::class);
$task->getId()->willReturn(123);
$task->getPerson()->willReturn($person);
$personBag = new \Chill\MainBundle\Audit\SubjectBag(new \Chill\MainBundle\Audit\Subject('person', ['id' => 456]));
$manager->getSubjectsForEntity($person, false)->willReturn($personBag);
$bag = $converter->convert($task->reveal(), true);
$this->assertSame('single_task', $bag->subject->type);
$this->assertCount(1, $bag->associatedSubjects);
$this->assertSame('person', $bag->associatedSubjects[0]->type);
}
public function testConvertWithCourse(): void
{
$converter = new SingleTaskSubjectConverter();
$manager = $this->prophesize(SubjectConverterManagerInterface::class);
$converter->setSubjectConverterManager($manager->reveal());
$course = $this->prophesize(AccompanyingPeriod::class)->reveal();
$task = $this->prophesize(SingleTask::class);
$task->getId()->willReturn(123);
$task->getPerson()->willReturn(null);
$task->getCourse()->willReturn($course);
$courseBag = new \Chill\MainBundle\Audit\SubjectBag(new \Chill\MainBundle\Audit\Subject('accompanying_period', ['id' => 789]));
$manager->getSubjectsForEntity($course, false)->willReturn($courseBag);
$bag = $converter->convert($task->reveal(), true);
$this->assertSame('single_task', $bag->subject->type);
$this->assertCount(1, $bag->associatedSubjects);
$this->assertSame('accompanying_period', $bag->associatedSubjects[0]->type);
}
}

View File

@@ -1,2 +1,7 @@
services:
_defaults:
autoconfigure: true
autowire: true
Chill\TaskBundle\Audit\:
resource: '../Audit/*'

View File

@@ -116,3 +116,9 @@ CHILL_TASK_TASK_UPDATE: Modifier une tâche
CHILL_TASK_TASK_CREATE_FOR_COURSE: Créer une tâche pour un parcours
CHILL_TASK_TASK_CREATE_FOR_PERSON: Créer une tâche pour un usager
audit:
single_task:
list_for_course: Liste des tâches pour un parcours
list_for_person: Liste des tâches pour un usager
not_found_with_id: Tâche non trouvée n°{id}
display: Tâche n°{id}