diff --git a/src/Bundle/ChillTaskBundle/Audit/Displayer/SingleTaskSubjectDisplayer.php b/src/Bundle/ChillTaskBundle/Audit/Displayer/SingleTaskSubjectDisplayer.php new file mode 100644 index 000000000..89f24a69b --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Audit/Displayer/SingleTaskSubjectDisplayer.php @@ -0,0 +1,44 @@ +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']); + } +} diff --git a/src/Bundle/ChillTaskBundle/Audit/SubjectConverter/SingleTaskSubjectConverter.php b/src/Bundle/ChillTaskBundle/Audit/SubjectConverter/SingleTaskSubjectConverter.php new file mode 100644 index 000000000..70d013a40 --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Audit/SubjectConverter/SingleTaskSubjectConverter.php @@ -0,0 +1,53 @@ + + */ +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; + } +} diff --git a/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php b/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php index debf0f1be..b3a8b4c80 100644 --- a/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php +++ b/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php @@ -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, diff --git a/src/Bundle/ChillTaskBundle/DependencyInjection/ChillTaskExtension.php b/src/Bundle/ChillTaskBundle/DependencyInjection/ChillTaskExtension.php index 0122861d6..29a60bcc7 100644 --- a/src/Bundle/ChillTaskBundle/DependencyInjection/ChillTaskExtension.php +++ b/src/Bundle/ChillTaskBundle/DependencyInjection/ChillTaskExtension.php @@ -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) diff --git a/src/Bundle/ChillTaskBundle/Resources/views/Audit/single_task.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/Audit/single_task.html.twig new file mode 100644 index 000000000..ec43a358d --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Resources/views/Audit/single_task.html.twig @@ -0,0 +1,13 @@ + + {% if task is not null %} + + {{ 'audit.single_task.display'|trans({'{id}': id} ) }} - + {{ task.title }} + + + + + {% else %} + {{ 'audit.single_task.not_found_with_id'|trans({'{id}': id}, 'messages') }} + {% endif %} + diff --git a/src/Bundle/ChillTaskBundle/Tests/Audit/SubjectConverter/SingleTaskSubjectConverterTest.php b/src/Bundle/ChillTaskBundle/Tests/Audit/SubjectConverter/SingleTaskSubjectConverterTest.php new file mode 100644 index 000000000..a14420363 --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Tests/Audit/SubjectConverter/SingleTaskSubjectConverterTest.php @@ -0,0 +1,99 @@ +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); + } +} diff --git a/src/Bundle/ChillTaskBundle/config/services.yaml b/src/Bundle/ChillTaskBundle/config/services.yaml index 5779e425e..efcd759c7 100644 --- a/src/Bundle/ChillTaskBundle/config/services.yaml +++ b/src/Bundle/ChillTaskBundle/config/services.yaml @@ -1,2 +1,7 @@ services: - + _defaults: + autoconfigure: true + autowire: true + + Chill\TaskBundle\Audit\: + resource: '../Audit/*' diff --git a/src/Bundle/ChillTaskBundle/translations/messages.fr.yml b/src/Bundle/ChillTaskBundle/translations/messages.fr.yml index 341e889ec..dcca9549e 100644 --- a/src/Bundle/ChillTaskBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillTaskBundle/translations/messages.fr.yml @@ -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}