mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge remote-tracking branch 'origin/339-partage-d'export-enregistré' into testing-202505
This commit is contained in:
commit
43e5bc8337
@ -63,6 +63,7 @@ framework:
|
||||
'Chill\MainBundle\Workflow\Messenger\PostPublicViewMessage': async
|
||||
'Chill\MainBundle\Service\Workflow\CancelStaleWorkflowMessage': async
|
||||
'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority
|
||||
'Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage': async
|
||||
# end of routes added by chill-bundles recipes
|
||||
# Route your messages to the transports
|
||||
# 'App\Message\YourMessage': async
|
||||
|
@ -12,17 +12,21 @@ declare(strict_types=1);
|
||||
namespace Chill\ActivityBundle\Export\Filter\ACPFilters;
|
||||
|
||||
use Chill\ActivityBundle\Export\Declarations;
|
||||
use Chill\MainBundle\Export\ExportDataNormalizerTrait;
|
||||
use Chill\MainBundle\Export\ExportGenerationContext;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
|
||||
use Chill\PersonBundle\Form\Type\PickSocialIssueType;
|
||||
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
|
||||
use Chill\PersonBundle\Templating\Entity\SocialIssueRender;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class BySocialIssueFilter implements FilterInterface
|
||||
{
|
||||
public function __construct(private readonly SocialIssueRender $issueRender) {}
|
||||
use ExportDataNormalizerTrait;
|
||||
|
||||
public function __construct(private readonly SocialIssueRender $issueRender, private readonly SocialIssueRepository $issueRepository) {}
|
||||
|
||||
public function addRole(): ?string
|
||||
{
|
||||
@ -63,12 +67,12 @@ class BySocialIssueFilter implements FilterInterface
|
||||
|
||||
public function normalizeFormData(array $formData): array
|
||||
{
|
||||
return ['accepted_socialissues' => $formData['accepted_socialissues']];
|
||||
return ['accepted_socialissues' => $this->normalizeDoctrineEntity($formData['accepted_socialissues'])];
|
||||
}
|
||||
|
||||
public function denormalizeFormData(array $formData, int $fromVersion): array
|
||||
{
|
||||
return ['accepted_socialissues' => $formData['accepted_socialissues']];
|
||||
return ['accepted_socialissues' => $this->denormalizeDoctrineEntity($formData['accepted_socialissues'], $this->issueRepository)];
|
||||
}
|
||||
|
||||
public function getFormDefaultData(): array
|
||||
|
@ -316,13 +316,14 @@ class ExportController extends AbstractController
|
||||
$savedExport,
|
||||
);
|
||||
|
||||
$this->entityManager->persist(
|
||||
$exportGeneration = new ExportGeneration(
|
||||
$alias,
|
||||
$this->exportConfigNormalizer->normalizeConfig($alias, $dataToNormalize),
|
||||
$this->clock->now()->add(new \DateInterval('P6M')),
|
||||
),
|
||||
);
|
||||
$deleteAt = $this->clock->now()->add(new \DateInterval('P6M'));
|
||||
$options = $this->exportConfigNormalizer->normalizeConfig($alias, $dataToNormalize);
|
||||
$exportGeneration = match (null === $savedExport) {
|
||||
true => new ExportGeneration($alias, $options, $deleteAt),
|
||||
false => ExportGeneration::fromSavedExport($savedExport, $deleteAt, $options),
|
||||
};
|
||||
|
||||
$this->entityManager->persist($exportGeneration);
|
||||
$this->entityManager->flush();
|
||||
$this->messageBus->dispatch(new ExportRequestGenerationMessage($exportGeneration, $user));
|
||||
|
||||
|
@ -14,13 +14,9 @@ namespace Chill\MainBundle\Controller;
|
||||
use Chill\MainBundle\Entity\ExportGeneration;
|
||||
use Chill\MainBundle\Entity\SavedExport;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Export\ExportInterface;
|
||||
use Chill\MainBundle\Export\ExportDescriptionHelper;
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
use Chill\MainBundle\Export\GroupedExportInterface;
|
||||
use Chill\MainBundle\Form\SavedExportType;
|
||||
use Chill\MainBundle\Repository\ExportGenerationRepository;
|
||||
use Chill\MainBundle\Repository\SavedExportRepositoryInterface;
|
||||
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
|
||||
use Chill\MainBundle\Security\Authorization\ExportGenerationVoter;
|
||||
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
@ -46,11 +42,10 @@ final readonly class SavedExportController
|
||||
private EntityManagerInterface $entityManager,
|
||||
private ExportManager $exportManager,
|
||||
private FormFactoryInterface $formFactory,
|
||||
private SavedExportRepositoryInterface $savedExportRepository,
|
||||
private Security $security,
|
||||
private TranslatorInterface $translator,
|
||||
private UrlGeneratorInterface $urlGenerator,
|
||||
private ExportGenerationRepository $exportGenerationRepository,
|
||||
private ExportDescriptionHelper $exportDescriptionHelper,
|
||||
) {}
|
||||
|
||||
#[Route(path: '/{_locale}/exports/saved/{id}/delete', name: 'chill_main_export_saved_delete')]
|
||||
@ -114,21 +109,35 @@ final readonly class SavedExportController
|
||||
$request->query->has('title') ? $request->query->get('title') : $title
|
||||
);
|
||||
|
||||
return $this->handleEdit($savedExport, $request);
|
||||
if ($exportGeneration->isLinkedToSavedExport()) {
|
||||
$savedExport->setDescription($exportGeneration->getSavedExport()->getDescription());
|
||||
} else {
|
||||
$savedExport->setDescription(
|
||||
implode(
|
||||
"\n",
|
||||
array_map(
|
||||
fn (string $item) => '- '.$item."\n",
|
||||
$this->exportDescriptionHelper->describe($savedExport->getExportAlias(), $savedExport->getOptions(), includeExportTitle: false)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $this->handleEdit($savedExport, $request, true);
|
||||
}
|
||||
|
||||
#[Route(path: '/exports/saved/duplicate-from-saved-export/{id}/new', name: 'chill_main_export_saved_duplicate')]
|
||||
public function duplicate(SavedExport $previousSavedExport, Request $request): Response
|
||||
{
|
||||
if (!$this->security->isGranted(SavedExportVoter::GENERATE, $previousSavedExport)) {
|
||||
throw new AccessDeniedHttpException('Not allowed to see this saved export');
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
if (!$user instanceof User) {
|
||||
throw new AccessDeniedHttpException('only regular user can create a saved export');
|
||||
}
|
||||
|
||||
if (!$this->security->isGranted(SavedExportVoter::EDIT, $previousSavedExport)) {
|
||||
throw new AccessDeniedHttpException('Not allowed to edit this saved export');
|
||||
}
|
||||
|
||||
$savedExport = new SavedExport();
|
||||
$savedExport
|
||||
->setExportAlias($previousSavedExport->getExportAlias())
|
||||
@ -145,7 +154,7 @@ final readonly class SavedExportController
|
||||
|
||||
}
|
||||
|
||||
private function handleEdit(SavedExport $savedExport, Request $request): Response
|
||||
private function handleEdit(SavedExport $savedExport, Request $request, bool $showWarningAutoGeneratedDescription = false): Response
|
||||
{
|
||||
$form = $this->formFactory->create(SavedExportType::class, $savedExport);
|
||||
$form->handleRequest($request);
|
||||
@ -168,6 +177,7 @@ final readonly class SavedExportController
|
||||
'@ChillMain/SavedExport/new.html.twig',
|
||||
[
|
||||
'form' => $form->createView(),
|
||||
'showWarningAutoGeneratedDescription' => $showWarningAutoGeneratedDescription,
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -209,7 +219,7 @@ final readonly class SavedExportController
|
||||
#[Route(path: '/{_locale}/exports/saved/{savedExport}/edit-options/{exportGeneration}', name: 'chill_main_export_saved_options_edit')]
|
||||
public function updateOptionsFromGeneration(SavedExport $savedExport, ExportGeneration $exportGeneration, Request $request): Response
|
||||
{
|
||||
if (!$this->security->isGranted(SavedExportVoter::EDIT, $savedExport)) {
|
||||
if (!$this->security->isGranted(SavedExportVoter::DUPLICATE, $savedExport)) {
|
||||
throw new AccessDeniedHttpException('You are not allowed to access this saved export');
|
||||
}
|
||||
|
||||
@ -234,52 +244,4 @@ final readonly class SavedExportController
|
||||
$this->urlGenerator->generate('chill_main_export_saved_edit', ['id' => $savedExport->getId()]),
|
||||
);
|
||||
}
|
||||
|
||||
#[Route(path: '/{_locale}/exports/saved/my', name: 'chill_main_export_saved_list_my')]
|
||||
public function list(): Response
|
||||
{
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$this->security->isGranted(ChillExportVoter::GENERATE_SAVED_EXPORT) || !$user instanceof User) {
|
||||
throw new AccessDeniedHttpException(sprintf('Missing role: %s', ChillExportVoter::GENERATE_SAVED_EXPORT));
|
||||
}
|
||||
|
||||
$exports = array_filter(
|
||||
$this->savedExportRepository->findSharedWithUser($user, ['exportAlias' => 'ASC', 'title' => 'ASC']),
|
||||
fn (SavedExport $savedExport): bool => $this->security->isGranted(SavedExportVoter::GENERATE, $savedExport),
|
||||
);
|
||||
|
||||
// group by center
|
||||
/** @var array<string, array{saved: SavedExport, export: ExportInterface}> $exportsGrouped */
|
||||
$exportsGrouped = [];
|
||||
|
||||
foreach ($exports as $savedExport) {
|
||||
$export = $this->exportManager->getExport($savedExport->getExportAlias());
|
||||
|
||||
$exportsGrouped[
|
||||
$export instanceof GroupedExportInterface
|
||||
? $this->translator->trans($export->getGroup()) : '_'
|
||||
][] = ['saved' => $savedExport, 'export' => $export];
|
||||
}
|
||||
|
||||
ksort($exportsGrouped);
|
||||
|
||||
// get last executions
|
||||
$lastExecutions = [];
|
||||
foreach ($exports as $savedExport) {
|
||||
$lastExecutions[$savedExport->getId()->toString()] = $this->exportGenerationRepository
|
||||
->findExportGenerationBySavedExportAndUser($savedExport, $user, 5);
|
||||
}
|
||||
|
||||
return new Response(
|
||||
$this->templating->render(
|
||||
'@ChillMain/SavedExport/index.html.twig',
|
||||
[
|
||||
'grouped_exports' => $exportsGrouped,
|
||||
'total' => \count($exports),
|
||||
'last_executions' => $lastExecutions,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,104 @@
|
||||
<?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\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\SavedExport;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Export\ExportInterface;
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
use Chill\MainBundle\Export\GroupedExportInterface;
|
||||
use Chill\MainBundle\Repository\ExportGenerationRepository;
|
||||
use Chill\MainBundle\Repository\SavedExportRepositoryInterface;
|
||||
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
|
||||
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
|
||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final readonly class SavedExportIndexController
|
||||
{
|
||||
public function __construct(
|
||||
private \Twig\Environment $templating,
|
||||
private ExportManager $exportManager,
|
||||
private SavedExportRepositoryInterface $savedExportRepository,
|
||||
private Security $security,
|
||||
private TranslatorInterface $translator,
|
||||
private ExportGenerationRepository $exportGenerationRepository,
|
||||
private FilterOrderHelperFactoryInterface $filterOrderHelperFactory,
|
||||
) {}
|
||||
|
||||
#[Route(path: '/{_locale}/exports/saved/my', name: 'chill_main_export_saved_list_my')]
|
||||
public function list(): Response
|
||||
{
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$this->security->isGranted(ChillExportVoter::GENERATE_SAVED_EXPORT) || !$user instanceof User) {
|
||||
throw new AccessDeniedHttpException(sprintf('Missing role: %s', ChillExportVoter::GENERATE_SAVED_EXPORT));
|
||||
}
|
||||
|
||||
$filter = $this->buildFilter();
|
||||
|
||||
$filterParams = [];
|
||||
if ('' !== $filter->getQueryString() && null !== $filter->getQueryString()) {
|
||||
$filterParams[SavedExportRepositoryInterface::FILTER_DESCRIPTION | SavedExportRepositoryInterface::FILTER_TITLE] = $filter->getQueryString();
|
||||
}
|
||||
|
||||
$exports = array_filter(
|
||||
$this->savedExportRepository->findSharedWithUser($user, ['exportAlias' => 'ASC', 'title' => 'ASC'], filters: $filterParams),
|
||||
fn (SavedExport $savedExport): bool => $this->security->isGranted(SavedExportVoter::GENERATE, $savedExport),
|
||||
);
|
||||
|
||||
// group by center
|
||||
/** @var array<string, array{saved: SavedExport, export: ExportInterface}> $exportsGrouped */
|
||||
$exportsGrouped = [];
|
||||
|
||||
foreach ($exports as $savedExport) {
|
||||
$export = $this->exportManager->getExport($savedExport->getExportAlias());
|
||||
|
||||
$exportsGrouped[$export instanceof GroupedExportInterface
|
||||
? $this->translator->trans($export->getGroup()) : '_'][] = ['saved' => $savedExport, 'export' => $export];
|
||||
}
|
||||
|
||||
ksort($exportsGrouped);
|
||||
|
||||
// get last executions
|
||||
$lastExecutions = [];
|
||||
foreach ($exports as $savedExport) {
|
||||
$lastExecutions[$savedExport->getId()->toString()] = $this->exportGenerationRepository
|
||||
->findExportGenerationBySavedExportAndUser($savedExport, $user, 5);
|
||||
}
|
||||
|
||||
return new Response(
|
||||
$this->templating->render(
|
||||
'@ChillMain/SavedExport/index.html.twig',
|
||||
[
|
||||
'grouped_exports' => $exportsGrouped,
|
||||
'total' => \count($exports),
|
||||
'last_executions' => $lastExecutions,
|
||||
'filter' => $filter,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private function buildFilter(): FilterOrderHelper
|
||||
{
|
||||
$filter = $this->filterOrderHelperFactory->create('saved-export-index-filter');
|
||||
$filter->addSearchBox();
|
||||
|
||||
return $filter->build();
|
||||
}
|
||||
}
|
@ -54,6 +54,13 @@ class ExportGeneration implements TrackCreationInterface
|
||||
private array $options = [],
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true)]
|
||||
private ?\DateTimeImmutable $deleteAt = null,
|
||||
|
||||
/**
|
||||
* The related saved export.
|
||||
*
|
||||
* Note that, in some case, the options of this ExportGenration are not equals to the options of the saved export.
|
||||
* This happens when the options of the saved export are updated.
|
||||
*/
|
||||
#[ORM\ManyToOne(targetEntity: SavedExport::class)]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
private ?SavedExport $savedExport = null,
|
||||
@ -118,9 +125,24 @@ class ExportGeneration implements TrackCreationInterface
|
||||
return null !== $this->savedExport;
|
||||
}
|
||||
|
||||
public static function fromSavedExport(SavedExport $savedExport, ?\DateTimeImmutable $deletedAt = null): self
|
||||
/**
|
||||
* Compares the options of the saved export and the current export generation.
|
||||
*
|
||||
* Return false if the current export generation's options are not equal to the one in the saved export. This may
|
||||
* happens when we update the configuration of a saved export.
|
||||
*/
|
||||
public function isConfigurationDifferentFromSavedExport(): bool
|
||||
{
|
||||
$generation = new self($savedExport->getExportAlias(), $savedExport->getOptions(), $deletedAt, $savedExport);
|
||||
if (!$this->isLinkedToSavedExport()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->savedExport->getOptions() !== $this->getOptions();
|
||||
}
|
||||
|
||||
public static function fromSavedExport(SavedExport $savedExport, ?\DateTimeImmutable $deletedAt = null, ?array $overrideOptions = null): self
|
||||
{
|
||||
$generation = new self($savedExport->getExportAlias(), $overrideOptions ?? $savedExport->getOptions(), $deletedAt, $savedExport);
|
||||
$generation->getStoredObject()->setTitle($savedExport->getTitle());
|
||||
|
||||
return $generation;
|
||||
|
@ -0,0 +1,52 @@
|
||||
<?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\MainBundle\Export\Cronjob;
|
||||
|
||||
use Chill\MainBundle\Cron\CronJobInterface;
|
||||
use Chill\MainBundle\Entity\CronJobExecution;
|
||||
use Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage;
|
||||
use Chill\MainBundle\Repository\ExportGenerationRepository;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
final readonly class RemoveExpiredExportGenerationCronJob implements CronJobInterface
|
||||
{
|
||||
public const KEY = 'remove-expired-export-generation';
|
||||
|
||||
public function __construct(private ClockInterface $clock, private ExportGenerationRepository $exportGenerationRepository, private MessageBusInterface $messageBus) {}
|
||||
|
||||
public function canRun(?CronJobExecution $cronJobExecution): bool
|
||||
{
|
||||
if (null === $cronJobExecution) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $cronJobExecution->getLastStart()->getTimestamp() < $this->clock->now()->sub(new \DateInterval('PT24H'))->getTimestamp();
|
||||
}
|
||||
|
||||
public function getKey(): string
|
||||
{
|
||||
return self::KEY;
|
||||
}
|
||||
|
||||
public function run(array $lastExecutionData): ?array
|
||||
{
|
||||
$now = $this->clock->now();
|
||||
|
||||
foreach ($this->exportGenerationRepository->findExpiredExportGeneration($now) as $exportGeneration) {
|
||||
$this->messageBus->dispatch(new Envelope(new RemoveExportGenerationMessage($exportGeneration)));
|
||||
}
|
||||
|
||||
return ['last-deletion' => $now->getTimestamp()];
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
<?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\MainBundle\Export;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* Give an explanation of an export.
|
||||
*/
|
||||
final readonly class ExportDescriptionHelper
|
||||
{
|
||||
public function __construct(
|
||||
private ExportManager $exportManager,
|
||||
private ExportConfigNormalizer $exportConfigNormalizer,
|
||||
private ExportConfigProcessor $exportConfigProcessor,
|
||||
private TranslatorInterface $translator,
|
||||
private Security $security,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function describe(string $exportAlias, array $exportOptions, bool $includeExportTitle = true): array
|
||||
{
|
||||
$output = [];
|
||||
$denormalized = $this->exportConfigNormalizer->denormalizeConfig($exportAlias, $exportOptions);
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if ($includeExportTitle) {
|
||||
$output[] = $this->trans($this->exportManager->getExport($exportAlias)->getTitle());
|
||||
}
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return $output;
|
||||
}
|
||||
$context = new ExportGenerationContext($user);
|
||||
|
||||
foreach ($this->exportConfigProcessor->retrieveUsedFilters($denormalized['filters']) as $name => $filter) {
|
||||
$output[] = $this->trans($filter->describeAction($denormalized['filters'][$name]['form'], $context));
|
||||
}
|
||||
|
||||
foreach ($this->exportConfigProcessor->retrieveUsedAggregators($denormalized['aggregators']) as $name => $aggregator) {
|
||||
$output[] = $this->trans($aggregator->getTitle());
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
private function trans(string|TranslatableInterface|array $translatable): string
|
||||
{
|
||||
if (is_string($translatable)) {
|
||||
return $this->translator->trans($translatable);
|
||||
}
|
||||
|
||||
if ($translatable instanceof TranslatableInterface) {
|
||||
return $translatable->trans($this->translator);
|
||||
}
|
||||
|
||||
// array case
|
||||
return $this->translator->trans($translatable[0], $translatable[1] ?? []);
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
<?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\MainBundle\Export\Messenger;
|
||||
|
||||
use Chill\MainBundle\Entity\ExportGeneration;
|
||||
|
||||
final readonly class RemoveExportGenerationMessage
|
||||
{
|
||||
public string $exportGenerationId;
|
||||
|
||||
public function __construct(ExportGeneration $exportGeneration)
|
||||
{
|
||||
$this->exportGenerationId = $exportGeneration->getId()->toString();
|
||||
}
|
||||
}
|
@ -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\MainBundle\Export\Messenger;
|
||||
|
||||
use Chill\MainBundle\Repository\ExportGenerationRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
|
||||
#[AsMessageHandler]
|
||||
class RemoveExportGenerationMessageHandler implements MessageHandlerInterface
|
||||
{
|
||||
private const LOG_PREFIX = '[RemoveExportGenerationMessageHandler] ';
|
||||
|
||||
public function __construct(
|
||||
private ExportGenerationRepository $exportGenerationRepository,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private LoggerInterface $logger,
|
||||
private ClockInterface $clock,
|
||||
) {}
|
||||
|
||||
public function __invoke(RemoveExportGenerationMessage $message): void
|
||||
{
|
||||
$exportGeneration = $this->exportGenerationRepository->find($message->exportGenerationId);
|
||||
|
||||
if (null === $exportGeneration) {
|
||||
$this->logger->error(self::LOG_PREFIX.'ExportGeneration not found');
|
||||
throw new UnrecoverableMessageHandlingException(self::LOG_PREFIX.'ExportGeneration not found');
|
||||
}
|
||||
|
||||
$storedObject = $exportGeneration->getStoredObject();
|
||||
$storedObject->setDeleteAt($this->clock->now());
|
||||
|
||||
$this->entityManager->remove($exportGeneration);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
}
|
@ -73,4 +73,13 @@ class ExportGenerationRepository extends ServiceEntityRepository implements Asso
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
public function findExpiredExportGeneration(\DateTimeImmutable $atDate): iterable
|
||||
{
|
||||
return $this->createQueryBuilder('e')
|
||||
->where('e.deleteAt < :atDate')
|
||||
->setParameter('atDate', $atDate)
|
||||
->getQuery()
|
||||
->toIterable();
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use Symfony\Component\String\UnicodeString;
|
||||
|
||||
/**
|
||||
* @implements ObjectRepository<SavedExport>
|
||||
@ -60,7 +61,7 @@ class SavedExportRepository implements SavedExportRepositoryInterface
|
||||
return $this->prepareResult($qb, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
||||
public function findSharedWithUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array
|
||||
public function findSharedWithUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null, array $filters = []): array
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('se');
|
||||
|
||||
@ -76,6 +77,27 @@ class SavedExportRepository implements SavedExportRepositoryInterface
|
||||
)
|
||||
->setParameter('user', $user);
|
||||
|
||||
foreach ($filters as $key => $filter) {
|
||||
if (self::FILTER_TITLE === ($key & self::FILTER_TITLE)
|
||||
|| self::FILTER_DESCRIPTION === ($key & self::FILTER_DESCRIPTION)) {
|
||||
$filter = new UnicodeString($filter);
|
||||
|
||||
$i = 0;
|
||||
foreach ($filter->split(' ') as $word) {
|
||||
$orx = $qb->expr()->orX();
|
||||
if (self::FILTER_TITLE === ($key & self::FILTER_TITLE)) {
|
||||
$orx->add($qb->expr()->like('LOWER(se.title)', 'LOWER(:qs'.$i.')'));
|
||||
}
|
||||
if (self::FILTER_DESCRIPTION === ($key & self::FILTER_DESCRIPTION)) {
|
||||
$orx->add($qb->expr()->like('LOWER(se.description)', 'LOWER(:qs'.$i.')'));
|
||||
}
|
||||
$qb->andWhere($orx);
|
||||
$qb->setParameter('qs'.$i, '%'.$word->trim().'%');
|
||||
++$i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->prepareResult($qb, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,9 @@ use Doctrine\Persistence\ObjectRepository;
|
||||
*/
|
||||
interface SavedExportRepositoryInterface extends ObjectRepository
|
||||
{
|
||||
public const FILTER_TITLE = 0x01;
|
||||
public const FILTER_DESCRIPTION = 0x10;
|
||||
|
||||
public function find($id): ?SavedExport;
|
||||
|
||||
/**
|
||||
@ -34,7 +37,14 @@ interface SavedExportRepositoryInterface extends ObjectRepository
|
||||
*/
|
||||
public function findByUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array;
|
||||
|
||||
public function findSharedWithUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array;
|
||||
/**
|
||||
* Get the saved export created by and the user and the ones shared with the user.
|
||||
*
|
||||
* @param array<int, mixed> $filters filters where keys are one of the constant starting with FILTER_
|
||||
*
|
||||
* @return list<SavedExport>
|
||||
*/
|
||||
public function findSharedWithUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null, array $filters = []): array;
|
||||
|
||||
public function findOneBy(array $criteria): ?SavedExport;
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
||||
<li v-if="isCurrentUserPicker" class="btn btn-sm btn-misc">
|
||||
<label class="flex items-center gap-2">
|
||||
<input
|
||||
:checked="picked.indexOf('me') >= 0 ? true : null"
|
||||
ref="itsMeCheckbox"
|
||||
:type="multiple ? 'checkbox' : 'radio'"
|
||||
@change="selectItsMe"
|
||||
|
@ -10,7 +10,10 @@
|
||||
{{ encore_entry_link_tags('page_download_exports') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block title exportGeneration.linkedToSavedExport ? exportGeneration.savedExport.title : 'Download export' %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ block('title') }}</h1>
|
||||
<div id="app"
|
||||
data-export-generation-id="{{ exportGeneration.id | escape('html_attr') }}"
|
||||
data-export-generation-date="{{ exportGeneration.createdAt.format('Ymd-His') }}"
|
||||
@ -23,13 +26,14 @@
|
||||
{{ 'export.generation.Come back later'|trans|chill_return_path_label }}
|
||||
</a>
|
||||
</li>
|
||||
{% if not exportGeneration.isLinkedToSavedExport %}
|
||||
{% if not exportGeneration.linkedToSavedExport %}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_main_export_saved_create_from_export_generation', {'id': exportGeneration.id}) }}" class="btn btn-save">
|
||||
{{ 'Save'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
{% if exportGeneration.configurationDifferentFromSavedExport %}
|
||||
<li>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-save dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">{{ 'Save'|trans }}</button>
|
||||
@ -52,5 +56,6 @@
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endblock content %}
|
||||
|
@ -18,6 +18,7 @@
|
||||
|
||||
{% block title %}{{ 'saved_export.Saved exports'|trans }}{% endblock %}
|
||||
|
||||
|
||||
{% macro render_export_card(saved, export, export_alias, generations) %}
|
||||
<div class="col">
|
||||
<div class="card h-100">
|
||||
@ -30,6 +31,10 @@
|
||||
<p class="card-text tags">
|
||||
{% if app.user is same as saved.user %}<span class="badge bg-primary">{{ 'saved_export.Owner'|trans }}</span>{% endif %}
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="card-text tags">
|
||||
Partagé par <span class="badge-user">{{ saved.user|chill_entity_render_box }}</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
<p class="card-text my-3">{{ saved.description|chill_markdown_to_html }}</p>
|
||||
</div>
|
||||
@ -63,7 +68,9 @@
|
||||
{% endif %}
|
||||
{# reminder: the controller already checked that the user can generate saved exports #}
|
||||
<li><a href="{{ chill_path_add_return_path('chill_main_export_new', {'alias': saved.exportAlias,'from_saved': saved.id }) }}" class="dropdown-item"><i class="fa fa-pencil"></i> {{ 'saved_export.update_filters_aggregators_and_execute'|trans }}</a></li>
|
||||
{% if is_granted('CHILL_MAIN_EXPORT_SAVED_DUPLICATE', saved) %}
|
||||
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_duplicate', {'id': saved.id}) }}" class="dropdown-item"><i class="fa fa-copy"></i> {{ 'saved_export.Duplicate'|trans }}</a></li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_MAIN_EXPORT_SAVED_DELETE', saved) %}
|
||||
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_delete', {'id': saved.id }) }}" class="dropdown-item"><i class="fa fa-trash"></i> {{ 'Delete'|trans }}</a></li>
|
||||
{% endif %}
|
||||
@ -82,6 +89,7 @@
|
||||
{{ include('@ChillMain/Export/_navbar.html.twig', {'current' : 'my'}) }}
|
||||
|
||||
<div class="container mt-4">
|
||||
{{ filter|chill_render_filter_order_helper }}
|
||||
|
||||
{% if total == 0 %}
|
||||
<p class="chill-no-data-statement" >{{ 'saved_export.Any saved export'|trans }}</p>
|
||||
|
@ -18,6 +18,13 @@
|
||||
|
||||
{{ form_start(form) }}
|
||||
{{ form_row(form.title) }}
|
||||
|
||||
{% if showWarningAutoGeneratedDescription|default(false) %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
{{ 'saved_export.Alert auto generated description'|trans }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{{ form_row(form.description) }}
|
||||
|
||||
{% if form.share is defined %}
|
||||
|
@ -15,6 +15,7 @@ use Chill\MainBundle\Entity\SavedExport;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
|
||||
final class SavedExportVoter extends Voter
|
||||
@ -25,6 +26,8 @@ final class SavedExportVoter extends Voter
|
||||
|
||||
final public const GENERATE = 'CHILL_MAIN_EXPORT_SAVED_GENERATE';
|
||||
|
||||
final public const DUPLICATE = 'CHILL_MAIN_EXPORT_SAVED_DUPLICATE';
|
||||
|
||||
final public const SHARE = 'CHILL_MAIN_EXPORT_SAVED_SHARE';
|
||||
|
||||
private const ALL = [
|
||||
@ -32,9 +35,10 @@ final class SavedExportVoter extends Voter
|
||||
self::EDIT,
|
||||
self::GENERATE,
|
||||
self::SHARE,
|
||||
self::DUPLICATE,
|
||||
];
|
||||
|
||||
public function __construct(private readonly ExportManager $exportManager) {}
|
||||
public function __construct(private readonly ExportManager $exportManager, private readonly AccessDecisionManagerInterface $accessDecisionManager) {}
|
||||
|
||||
protected function supports($attribute, $subject): bool
|
||||
{
|
||||
@ -52,6 +56,7 @@ final class SavedExportVoter extends Voter
|
||||
|
||||
return match ($attribute) {
|
||||
self::DELETE, self::EDIT, self::SHARE => $subject->getUser() === $token->getUser(),
|
||||
self::DUPLICATE => $this->accessDecisionManager->decide($token, [ChillExportVoter::COMPOSE_EXPORT]) && $this->accessDecisionManager->decide($token, [self::EDIT], $subject) ,
|
||||
self::GENERATE => $this->canUserGenerate($user, $subject),
|
||||
default => throw new \UnexpectedValueException('attribute not supported: '.$attribute),
|
||||
};
|
||||
|
@ -87,8 +87,8 @@ final readonly class UserGroupRelatedToUserJobSync implements UserGroupRelatedTo
|
||||
AND chill_main_user_group_user.user_id = u.id
|
||||
AND jh.user_id = u.id AND tsrange(jh.startdate, jh.enddate) @> localtimestamp
|
||||
-- only when the user's jobid is different than the user_group id
|
||||
AND ug.userjob_id IS NOT NULL
|
||||
AND jh.job_id <> ug.userjob_id
|
||||
-- or where the user.enabled is null
|
||||
AND ((ug.userjob_id IS NOT NULL AND jh.job_id <> ug.userjob_id) OR u.enabled IS NULL)
|
||||
SQL;
|
||||
|
||||
$result = $connection->executeQuery($sql);
|
||||
@ -103,7 +103,8 @@ final readonly class UserGroupRelatedToUserJobSync implements UserGroupRelatedTo
|
||||
SELECT cmug.id, jh.user_id
|
||||
FROM chill_main_user_group cmug
|
||||
JOIN chill_main_user_job_history jh ON jh.job_id = cmug.userjob_id AND tsrange(jh.startdate, jh.enddate) @> localtimestamp
|
||||
WHERE cmug.userjob_id IS NOT NULL
|
||||
JOIN users u ON u.id = jh.user_id
|
||||
WHERE cmug.userjob_id IS NOT NULL AND u.enabled IS TRUE
|
||||
ON CONFLICT DO NOTHING
|
||||
SQL;
|
||||
|
||||
|
@ -0,0 +1,118 @@
|
||||
<?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\MainBundle\Tests\Export\Cronjob;
|
||||
|
||||
use Chill\MainBundle\Entity\CronJobExecution;
|
||||
use Chill\MainBundle\Entity\ExportGeneration;
|
||||
use Chill\MainBundle\Export\Cronjob\RemoveExpiredExportGenerationCronJob;
|
||||
use Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Argument;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Chill\MainBundle\Repository\ExportGenerationRepository;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class RemoveExpiredExportGenerationCronJobTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testCanRunReturnsTrueWhenLastExecutionIsNull()
|
||||
{
|
||||
$clock = new MockClock(new \DateTimeImmutable('2024-06-25 10:00:00'));
|
||||
$repo = $this->prophesize(ExportGenerationRepository::class);
|
||||
$bus = $this->prophesize(MessageBusInterface::class);
|
||||
|
||||
$cronJob = new RemoveExpiredExportGenerationCronJob(
|
||||
$clock,
|
||||
$repo->reveal(),
|
||||
$bus->reveal()
|
||||
);
|
||||
|
||||
$this->assertTrue($cronJob->canRun(null));
|
||||
}
|
||||
|
||||
public function testCanRunReturnsTrueWhenLastStartIsOlderThan24Hours()
|
||||
{
|
||||
$clock = new MockClock(new \DateTimeImmutable('2024-06-25 10:00:00'));
|
||||
$repo = $this->prophesize(ExportGenerationRepository::class);
|
||||
$bus = $this->prophesize(MessageBusInterface::class);
|
||||
|
||||
$cronJob = new RemoveExpiredExportGenerationCronJob(
|
||||
$clock,
|
||||
$repo->reveal(),
|
||||
$bus->reveal()
|
||||
);
|
||||
|
||||
$execution = new CronJobExecution('remove-expired-export-generation');
|
||||
$execution->setLastStart(new \DateTimeImmutable('2024-06-24 09:59:59'));
|
||||
|
||||
$this->assertTrue($cronJob->canRun($execution));
|
||||
}
|
||||
|
||||
public function testCanRunReturnsFalseWhenLastStartIsWithin24Hours()
|
||||
{
|
||||
$clock = new MockClock(new \DateTimeImmutable('2024-06-25 10:00:00'));
|
||||
$repo = $this->prophesize(ExportGenerationRepository::class);
|
||||
$bus = $this->prophesize(MessageBusInterface::class);
|
||||
|
||||
$cronJob = new RemoveExpiredExportGenerationCronJob(
|
||||
$clock,
|
||||
$repo->reveal(),
|
||||
$bus->reveal()
|
||||
);
|
||||
|
||||
$execution = new CronJobExecution('remove-expired-export-generation');
|
||||
$execution->setLastStart(new \DateTimeImmutable('2024-06-24 10:01:00'));
|
||||
|
||||
$this->assertFalse($cronJob->canRun($execution));
|
||||
}
|
||||
|
||||
public function testRunDispatchesMessagesForExpiredExportsAndReturnsLastDeletion()
|
||||
{
|
||||
$clock = new MockClock(new \DateTimeImmutable('2024-06-25 11:21:00'));
|
||||
$repo = $this->prophesize(ExportGenerationRepository::class);
|
||||
$bus = $this->prophesize(MessageBusInterface::class);
|
||||
|
||||
$expiredExports = [
|
||||
new ExportGeneration('dummy', []),
|
||||
];
|
||||
|
||||
$repo->findExpiredExportGeneration(Argument::that(function ($dateTime) use ($clock) {
|
||||
// Ensure the repository is called with the current clock time
|
||||
return $dateTime instanceof \DateTimeImmutable
|
||||
&& $dateTime->getTimestamp() === $clock->now()->getTimestamp();
|
||||
}))->willReturn($expiredExports);
|
||||
|
||||
// Expect one RemoveExportGenerationMessage for each expired export
|
||||
$bus->dispatch(Argument::that(fn (Envelope $envelope) => $envelope->getMessage() instanceof RemoveExportGenerationMessage))
|
||||
->shouldBeCalledTimes(1)
|
||||
->will(fn ($args) => $args[0]);
|
||||
|
||||
$cronJob = new RemoveExpiredExportGenerationCronJob(
|
||||
$clock,
|
||||
$repo->reveal(),
|
||||
$bus->reveal()
|
||||
);
|
||||
|
||||
$result = $cronJob->run([]);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertEquals(['last-deletion' => $clock->now()->getTimestamp()], $result);
|
||||
}
|
||||
}
|
@ -0,0 +1,174 @@
|
||||
<?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\MainBundle\Tests\Export;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Export\AggregatorInterface;
|
||||
use Chill\MainBundle\Export\ExportConfigNormalizer;
|
||||
use Chill\MainBundle\Export\ExportConfigProcessor;
|
||||
use Chill\MainBundle\Export\ExportDescriptionHelper;
|
||||
use Chill\MainBundle\Export\ExportGenerationContext;
|
||||
use Chill\MainBundle\Export\ExportInterface;
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
use Chill\MainBundle\Export\FilterInterface;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class ExportDescriptionHelperTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
private const JSON_HAPPY_SCENARIO = <<<'JSON'
|
||||
{
|
||||
"export": {
|
||||
"form": [],
|
||||
"version": 1
|
||||
},
|
||||
"centers": {
|
||||
"centers": [
|
||||
1
|
||||
],
|
||||
"regroupments": []
|
||||
},
|
||||
"filters": {
|
||||
"my_filter_string": {
|
||||
"form": {
|
||||
"accepted_socialissues": [
|
||||
2,
|
||||
3
|
||||
]
|
||||
},
|
||||
"enabled": true,
|
||||
"version": 1
|
||||
},
|
||||
"my_filter_array": {
|
||||
"form": {
|
||||
"misc": true
|
||||
},
|
||||
"enabled": true,
|
||||
"version": 1
|
||||
},
|
||||
"my_filter_translatable": {
|
||||
"form": {
|
||||
"misc": true
|
||||
},
|
||||
"enabled": true,
|
||||
"version": 1
|
||||
}
|
||||
},
|
||||
"formatter": {
|
||||
"form": {
|
||||
"format": "xlsx",
|
||||
"activity_user_aggregator": {
|
||||
"order": 1
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
},
|
||||
"aggregators": {
|
||||
"my_aggregator": {
|
||||
"form": {"key": 1},
|
||||
"enabled": true,
|
||||
"version": 1
|
||||
}
|
||||
},
|
||||
"pick_formatter": "spreadsheet"
|
||||
}
|
||||
JSON;
|
||||
|
||||
public function testDescribeHappyScenario(): void
|
||||
{
|
||||
$options = json_decode(self::JSON_HAPPY_SCENARIO, true);
|
||||
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->getUser()->willReturn($user = new User());
|
||||
|
||||
$exportConfigNormalizer = $this->prophesize(ExportConfigNormalizer::class);
|
||||
$exportConfigNormalizer->denormalizeConfig('my_export', Argument::type('array'))->willReturn($options);
|
||||
|
||||
$export = $this->prophesize(ExportInterface::class);
|
||||
$export->getTitle()->willReturn('Title');
|
||||
|
||||
$myFilterString = $this->prophesize(FilterInterface::class);
|
||||
$myFilterString->describeAction(Argument::type('array'), Argument::type(ExportGenerationContext::class))->willReturn($string0 = 'This is a filter description');
|
||||
$myFilterArray = $this->prophesize(FilterInterface::class);
|
||||
$myFilterArray->describeAction(Argument::type('array'), Argument::type(ExportGenerationContext::class))->willReturn([$string1 = 'This is a filter with %argument%', $arg1 = ['%argument%' => 'zero']]);
|
||||
$myFilterTranslatable = $this->prophesize(FilterInterface::class);
|
||||
$myFilterTranslatable->describeAction(Argument::type('array'), Argument::type(ExportGenerationContext::class))
|
||||
->willReturn(new class () implements TranslatableInterface {
|
||||
public function trans(TranslatorInterface $translator, ?string $locale = null): string
|
||||
{
|
||||
return 'translatable';
|
||||
}
|
||||
});
|
||||
|
||||
$myAggregator = $this->prophesize(AggregatorInterface::class);
|
||||
$myAggregator->getTitle()->willReturn('Some aggregator');
|
||||
|
||||
$token = new UsernamePasswordToken($user, 'main', ['ROLE_USER']);
|
||||
$tokenStorage = new TokenStorage();
|
||||
$tokenStorage->setToken($token);
|
||||
|
||||
$exportManager = new ExportManager(
|
||||
new NullLogger(),
|
||||
$security->reveal(),
|
||||
$this->prophesize(AuthorizationHelperInterface::class)->reveal(),
|
||||
$tokenStorage,
|
||||
['my_export' => $export->reveal()],
|
||||
['my_aggregator' => $myAggregator->reveal()],
|
||||
[
|
||||
'my_filter_string' => $myFilterString->reveal(),
|
||||
'my_filter_array' => $myFilterArray->reveal(),
|
||||
'my_filter_translatable' => $myFilterTranslatable->reveal(),
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
$exportConfigProcessor = new ExportConfigProcessor($exportManager);
|
||||
|
||||
$translator = $this->prophesize(TranslatorInterface::class);
|
||||
$translator->trans('Title')->shouldBeCalled()->willReturn('Title');
|
||||
$translator->trans($string0)->shouldBeCalled()->willReturn($string0);
|
||||
$translator->trans($string1, $arg1)->shouldBeCalled()->willReturn($string1);
|
||||
$translator->trans('Some aggregator')->shouldBeCalled()->willReturn('Some aggregator');
|
||||
|
||||
$exportDescriptionHelper = new ExportDescriptionHelper(
|
||||
$exportManager,
|
||||
$exportConfigNormalizer->reveal(),
|
||||
$exportConfigProcessor,
|
||||
$translator->reveal(),
|
||||
$security->reveal(),
|
||||
);
|
||||
|
||||
$actual = $exportDescriptionHelper->describe('my_export', $options);
|
||||
|
||||
self::assertIsArray($actual);
|
||||
self::assertEquals($actual[0], 'Title');
|
||||
self::assertEquals($actual[1], 'This is a filter description');
|
||||
self::assertEquals($actual[2], 'This is a filter with %argument%');
|
||||
self::assertEquals($actual[3], 'translatable');
|
||||
self::assertEquals($actual[4], 'Some aggregator');
|
||||
}
|
||||
}
|
@ -348,7 +348,6 @@ final class ExportManagerTest extends KernelTestCase
|
||||
$logger ?? self::getContainer()->get(LoggerInterface::class),
|
||||
$authorizationChecker ?? self::getContainer()->get('security.authorization_checker'),
|
||||
$authorizationHelper ?? self::getContainer()->get('chill.main.security.authorization.helper'),
|
||||
$tokenStorage,
|
||||
$exports,
|
||||
$aggregators,
|
||||
$filters,
|
||||
|
@ -0,0 +1,76 @@
|
||||
<?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\MainBundle\Tests\Export\Messenger;
|
||||
|
||||
use Chill\MainBundle\Entity\ExportGeneration;
|
||||
use Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage;
|
||||
use Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessageHandler;
|
||||
use Chill\MainBundle\Repository\ExportGenerationRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class RemoveExportGenerationMessageHandlerTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testInvokeUpdatesDeleteAtAndRemovesAndFlushes()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// 1. Create a MockClock at a fixed point in time
|
||||
$now = new \DateTimeImmutable('2024-06-01T12:00:00');
|
||||
$clock = new MockClock($now);
|
||||
|
||||
// 2. Create an ExportGeneration entity with a stored object
|
||||
$exportGeneration = new ExportGeneration('test-alias', ['foo' => 'bar']);
|
||||
$storedObject = $exportGeneration->getStoredObject();
|
||||
|
||||
// 3. Mock ExportGenerationRepository to return the ExportGeneration
|
||||
$exportGenerationRepository = $this->prophesize(ExportGenerationRepository::class);
|
||||
$exportGenerationRepository
|
||||
->find($exportGeneration->getId())
|
||||
->willReturn($exportGeneration);
|
||||
|
||||
// 4. Mock EntityManagerInterface and set expectations
|
||||
$entityManager = $this->prophesize(EntityManagerInterface::class);
|
||||
$entityManager->remove($exportGeneration)->shouldBeCalled();
|
||||
$entityManager->flush()->shouldBeCalled();
|
||||
|
||||
// 6. Create message
|
||||
$message = new RemoveExportGenerationMessage($exportGeneration);
|
||||
|
||||
// 7. Handler instantiation
|
||||
$handler = new RemoveExportGenerationMessageHandler(
|
||||
$exportGenerationRepository->reveal(),
|
||||
$entityManager->reveal(),
|
||||
new NullLogger(),
|
||||
$clock
|
||||
);
|
||||
|
||||
// Pre-condition: deleteAt not set.
|
||||
$this->assertNull($storedObject->getDeleteAt());
|
||||
|
||||
// Act
|
||||
$handler->__invoke($message);
|
||||
|
||||
// Assert
|
||||
$this->assertEquals($now, $storedObject->getDeleteAt(), 'deleteAt of stored object was updated');
|
||||
}
|
||||
}
|
@ -6,8 +6,13 @@ services:
|
||||
Chill\MainBundle\Export\Helper\:
|
||||
resource: '../../Export/Helper'
|
||||
|
||||
Chill\MainBundle\Export\Cronjob\:
|
||||
resource: '../../Export/Cronjob'
|
||||
|
||||
Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessageHandler: ~
|
||||
|
||||
Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessageHandler: ~
|
||||
|
||||
Chill\MainBundle\Export\Messenger\OnExportGenerationFails: ~
|
||||
|
||||
Chill\MainBundle\Export\ExportFormHelper: ~
|
||||
@ -18,6 +23,8 @@ services:
|
||||
|
||||
Chill\MainBundle\Export\ExportConfigProcessor: ~
|
||||
|
||||
Chill\MainBundle\Export\ExportDescriptionHelper: ~
|
||||
|
||||
chill.main.export_element_validator:
|
||||
class: Chill\MainBundle\Validator\Constraints\Export\ExportElementConstraintValidator
|
||||
tags:
|
||||
|
@ -809,6 +809,7 @@ saved_export:
|
||||
Duplicated: Dupliqué
|
||||
Options updated successfully: La configuration de l'export a été mise à jour
|
||||
Share: Partage
|
||||
Alert auto generated description: La description ci-dessous a été générée automatiquement, comme si l'export était exécutée immédiatement. Veillez à l'adapter pour tenir compte des paramètres qui peuvent être modifiés (utilisateurs courant, dates glissantes, etc.).
|
||||
|
||||
absence:
|
||||
# single letter for absence
|
||||
|
Loading…
x
Reference in New Issue
Block a user