mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2026-03-03 12:39:42 +00:00
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`.
This commit is contained in:
@@ -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]]
|
||||
);
|
||||
```
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<?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\ActivityBundle\Audit\Converter;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
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;
|
||||
|
||||
final class ActivitySubjectConverter implements SubjectConverterInterface, SubjectConverterManagerAwareInterface
|
||||
{
|
||||
use SubjectConverterManagerAwareTrait;
|
||||
|
||||
public function supportsConvert(mixed $subject): bool
|
||||
{
|
||||
return $subject instanceof Activity;
|
||||
}
|
||||
|
||||
public function convert(mixed $subject, bool $includeAssociated = false): SubjectBag
|
||||
{
|
||||
\assert($subject instanceof Activity);
|
||||
|
||||
$main = new Subject(
|
||||
type: 'activity',
|
||||
identifiers: ['id' => $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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?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\ActivityBundle\Audit\Displayer;
|
||||
|
||||
use Chill\ActivityBundle\Repository\ActivityRepository;
|
||||
use Chill\MainBundle\Audit\Subject;
|
||||
use Chill\MainBundle\Audit\SubjectDisplayerInterface;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final class ActivityDisplayer implements SubjectDisplayerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TranslatorInterface $translator,
|
||||
private readonly ActivityRepository $activityRepository,
|
||||
private readonly TranslatableStringHelperInterface $translatableStringHelper,
|
||||
) {}
|
||||
|
||||
public function supportsDisplay(Subject $subject, array $options = []): bool
|
||||
{
|
||||
return 'activity' === $subject->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 ? '<span>'.$label.'</span>' : $label;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -39,5 +39,8 @@ services:
|
||||
Chill\ActivityBundle\Service\EntityInfo\:
|
||||
resource: '../Service/EntityInfo/'
|
||||
|
||||
Chill\ActivityBundle\Audit\:
|
||||
resource: '../Audit/'
|
||||
|
||||
Chill\ActivityBundle\Service\GenericDoc\:
|
||||
resource: '../Service/GenericDoc/'
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user