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\Workflow\Messenger\PostPublicViewMessage': async
|
||||||
'Chill\MainBundle\Service\Workflow\CancelStaleWorkflowMessage': async
|
'Chill\MainBundle\Service\Workflow\CancelStaleWorkflowMessage': async
|
||||||
'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority
|
'Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage': priority
|
||||||
|
'Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessage': async
|
||||||
# end of routes added by chill-bundles recipes
|
# end of routes added by chill-bundles recipes
|
||||||
# Route your messages to the transports
|
# Route your messages to the transports
|
||||||
# 'App\Message\YourMessage': async
|
# 'App\Message\YourMessage': async
|
||||||
|
@ -12,17 +12,21 @@ declare(strict_types=1);
|
|||||||
namespace Chill\ActivityBundle\Export\Filter\ACPFilters;
|
namespace Chill\ActivityBundle\Export\Filter\ACPFilters;
|
||||||
|
|
||||||
use Chill\ActivityBundle\Export\Declarations;
|
use Chill\ActivityBundle\Export\Declarations;
|
||||||
|
use Chill\MainBundle\Export\ExportDataNormalizerTrait;
|
||||||
use Chill\MainBundle\Export\ExportGenerationContext;
|
use Chill\MainBundle\Export\ExportGenerationContext;
|
||||||
use Chill\MainBundle\Export\FilterInterface;
|
use Chill\MainBundle\Export\FilterInterface;
|
||||||
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
|
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
|
||||||
use Chill\PersonBundle\Form\Type\PickSocialIssueType;
|
use Chill\PersonBundle\Form\Type\PickSocialIssueType;
|
||||||
|
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
|
||||||
use Chill\PersonBundle\Templating\Entity\SocialIssueRender;
|
use Chill\PersonBundle\Templating\Entity\SocialIssueRender;
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
|
||||||
class BySocialIssueFilter implements FilterInterface
|
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
|
public function addRole(): ?string
|
||||||
{
|
{
|
||||||
@ -63,12 +67,12 @@ class BySocialIssueFilter implements FilterInterface
|
|||||||
|
|
||||||
public function normalizeFormData(array $formData): array
|
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
|
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
|
public function getFormDefaultData(): array
|
||||||
|
@ -316,13 +316,14 @@ class ExportController extends AbstractController
|
|||||||
$savedExport,
|
$savedExport,
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->entityManager->persist(
|
$deleteAt = $this->clock->now()->add(new \DateInterval('P6M'));
|
||||||
$exportGeneration = new ExportGeneration(
|
$options = $this->exportConfigNormalizer->normalizeConfig($alias, $dataToNormalize);
|
||||||
$alias,
|
$exportGeneration = match (null === $savedExport) {
|
||||||
$this->exportConfigNormalizer->normalizeConfig($alias, $dataToNormalize),
|
true => new ExportGeneration($alias, $options, $deleteAt),
|
||||||
$this->clock->now()->add(new \DateInterval('P6M')),
|
false => ExportGeneration::fromSavedExport($savedExport, $deleteAt, $options),
|
||||||
),
|
};
|
||||||
);
|
|
||||||
|
$this->entityManager->persist($exportGeneration);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
$this->messageBus->dispatch(new ExportRequestGenerationMessage($exportGeneration, $user));
|
$this->messageBus->dispatch(new ExportRequestGenerationMessage($exportGeneration, $user));
|
||||||
|
|
||||||
|
@ -14,13 +14,9 @@ namespace Chill\MainBundle\Controller;
|
|||||||
use Chill\MainBundle\Entity\ExportGeneration;
|
use Chill\MainBundle\Entity\ExportGeneration;
|
||||||
use Chill\MainBundle\Entity\SavedExport;
|
use Chill\MainBundle\Entity\SavedExport;
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Export\ExportInterface;
|
use Chill\MainBundle\Export\ExportDescriptionHelper;
|
||||||
use Chill\MainBundle\Export\ExportManager;
|
use Chill\MainBundle\Export\ExportManager;
|
||||||
use Chill\MainBundle\Export\GroupedExportInterface;
|
|
||||||
use Chill\MainBundle\Form\SavedExportType;
|
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\ExportGenerationVoter;
|
||||||
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
|
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
@ -46,11 +42,10 @@ final readonly class SavedExportController
|
|||||||
private EntityManagerInterface $entityManager,
|
private EntityManagerInterface $entityManager,
|
||||||
private ExportManager $exportManager,
|
private ExportManager $exportManager,
|
||||||
private FormFactoryInterface $formFactory,
|
private FormFactoryInterface $formFactory,
|
||||||
private SavedExportRepositoryInterface $savedExportRepository,
|
|
||||||
private Security $security,
|
private Security $security,
|
||||||
private TranslatorInterface $translator,
|
private TranslatorInterface $translator,
|
||||||
private UrlGeneratorInterface $urlGenerator,
|
private UrlGeneratorInterface $urlGenerator,
|
||||||
private ExportGenerationRepository $exportGenerationRepository,
|
private ExportDescriptionHelper $exportDescriptionHelper,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Route(path: '/{_locale}/exports/saved/{id}/delete', name: 'chill_main_export_saved_delete')]
|
#[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
|
$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')]
|
#[Route(path: '/exports/saved/duplicate-from-saved-export/{id}/new', name: 'chill_main_export_saved_duplicate')]
|
||||||
public function duplicate(SavedExport $previousSavedExport, Request $request): Response
|
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();
|
$user = $this->security->getUser();
|
||||||
if (!$user instanceof User) {
|
if (!$user instanceof User) {
|
||||||
throw new AccessDeniedHttpException('only regular user can create a saved export');
|
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 = new SavedExport();
|
||||||
$savedExport
|
$savedExport
|
||||||
->setExportAlias($previousSavedExport->getExportAlias())
|
->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 = $this->formFactory->create(SavedExportType::class, $savedExport);
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
@ -168,6 +177,7 @@ final readonly class SavedExportController
|
|||||||
'@ChillMain/SavedExport/new.html.twig',
|
'@ChillMain/SavedExport/new.html.twig',
|
||||||
[
|
[
|
||||||
'form' => $form->createView(),
|
'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')]
|
#[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
|
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');
|
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()]),
|
$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 = [],
|
private array $options = [],
|
||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true)]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true)]
|
||||||
private ?\DateTimeImmutable $deleteAt = null,
|
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\ManyToOne(targetEntity: SavedExport::class)]
|
||||||
#[ORM\JoinColumn(nullable: true)]
|
#[ORM\JoinColumn(nullable: true)]
|
||||||
private ?SavedExport $savedExport = null,
|
private ?SavedExport $savedExport = null,
|
||||||
@ -118,9 +125,24 @@ class ExportGeneration implements TrackCreationInterface
|
|||||||
return null !== $this->savedExport;
|
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());
|
$generation->getStoredObject()->setTitle($savedExport->getTitle());
|
||||||
|
|
||||||
return $generation;
|
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()
|
->getQuery()
|
||||||
->getResult();
|
->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\EntityRepository;
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Doctrine\Persistence\ObjectRepository;
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
use Symfony\Component\String\UnicodeString;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @implements ObjectRepository<SavedExport>
|
* @implements ObjectRepository<SavedExport>
|
||||||
@ -60,7 +61,7 @@ class SavedExportRepository implements SavedExportRepositoryInterface
|
|||||||
return $this->prepareResult($qb, $orderBy, $limit, $offset);
|
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');
|
$qb = $this->repository->createQueryBuilder('se');
|
||||||
|
|
||||||
@ -76,6 +77,27 @@ class SavedExportRepository implements SavedExportRepositoryInterface
|
|||||||
)
|
)
|
||||||
->setParameter('user', $user);
|
->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);
|
return $this->prepareResult($qb, $orderBy, $limit, $offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,9 @@ use Doctrine\Persistence\ObjectRepository;
|
|||||||
*/
|
*/
|
||||||
interface SavedExportRepositoryInterface extends ObjectRepository
|
interface SavedExportRepositoryInterface extends ObjectRepository
|
||||||
{
|
{
|
||||||
|
public const FILTER_TITLE = 0x01;
|
||||||
|
public const FILTER_DESCRIPTION = 0x10;
|
||||||
|
|
||||||
public function find($id): ?SavedExport;
|
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 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;
|
public function findOneBy(array $criteria): ?SavedExport;
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
<li v-if="isCurrentUserPicker" class="btn btn-sm btn-misc">
|
<li v-if="isCurrentUserPicker" class="btn btn-sm btn-misc">
|
||||||
<label class="flex items-center gap-2">
|
<label class="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
|
:checked="picked.indexOf('me') >= 0 ? true : null"
|
||||||
ref="itsMeCheckbox"
|
ref="itsMeCheckbox"
|
||||||
:type="multiple ? 'checkbox' : 'radio'"
|
:type="multiple ? 'checkbox' : 'radio'"
|
||||||
@change="selectItsMe"
|
@change="selectItsMe"
|
||||||
|
@ -10,7 +10,10 @@
|
|||||||
{{ encore_entry_link_tags('page_download_exports') }}
|
{{ encore_entry_link_tags('page_download_exports') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block title exportGeneration.linkedToSavedExport ? exportGeneration.savedExport.title : 'Download export' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<h1>{{ block('title') }}</h1>
|
||||||
<div id="app"
|
<div id="app"
|
||||||
data-export-generation-id="{{ exportGeneration.id | escape('html_attr') }}"
|
data-export-generation-id="{{ exportGeneration.id | escape('html_attr') }}"
|
||||||
data-export-generation-date="{{ exportGeneration.createdAt.format('Ymd-His') }}"
|
data-export-generation-date="{{ exportGeneration.createdAt.format('Ymd-His') }}"
|
||||||
@ -23,34 +26,36 @@
|
|||||||
{{ 'export.generation.Come back later'|trans|chill_return_path_label }}
|
{{ 'export.generation.Come back later'|trans|chill_return_path_label }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% if not exportGeneration.isLinkedToSavedExport %}
|
{% if not exportGeneration.linkedToSavedExport %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ chill_path_add_return_path('chill_main_export_saved_create_from_export_generation', {'id': exportGeneration.id}) }}" class="btn btn-save">
|
<a href="{{ chill_path_add_return_path('chill_main_export_saved_create_from_export_generation', {'id': exportGeneration.id}) }}" class="btn btn-save">
|
||||||
{{ 'Save'|trans }}
|
{{ 'Save'|trans }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li>
|
{% if exportGeneration.configurationDifferentFromSavedExport %}
|
||||||
<div class="dropdown">
|
<li>
|
||||||
<button class="btn btn-save dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">{{ 'Save'|trans }}</button>
|
<div class="dropdown">
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<button class="btn btn-save dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">{{ 'Save'|trans }}</button>
|
||||||
<li class="dropdown-item">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<a href="{{ chill_path_add_return_path('chill_main_export_saved_create_from_export_generation', {'id': exportGeneration.id, 'title': exportGeneration.savedExport.title ~ ' (' ~ 'saved_export.Duplicated'|trans ~ ' ' ~ null|format_datetime('short', 'medium') ~ ')'}) }}" class="btn">
|
|
||||||
<i class="bi bi-copy"></i> {{ 'saved_export.Save to new saved export'|trans }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% if is_granted('CHILL_MAIN_EXPORT_SAVED_EDIT', exportGeneration.savedExport) %}
|
|
||||||
<li class="dropdown-item">
|
<li class="dropdown-item">
|
||||||
<form method="POST" action="{{ path('chill_main_export_saved_options_edit', {'savedExport': exportGeneration.savedExport.id, 'exportGeneration': exportGeneration.id }) }}">
|
<a href="{{ chill_path_add_return_path('chill_main_export_saved_create_from_export_generation', {'id': exportGeneration.id, 'title': exportGeneration.savedExport.title ~ ' (' ~ 'saved_export.Duplicated'|trans ~ ' ' ~ null|format_datetime('short', 'medium') ~ ')'}) }}" class="btn">
|
||||||
<button type="submit" class="btn">
|
<i class="bi bi-copy"></i> {{ 'saved_export.Save to new saved export'|trans }}
|
||||||
<i class="bi bi-floppy"></i> {{ 'saved_export.Update current saved export'|trans }}
|
</a>
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% if is_granted('CHILL_MAIN_EXPORT_SAVED_EDIT', exportGeneration.savedExport) %}
|
||||||
</ul>
|
<li class="dropdown-item">
|
||||||
</div>
|
<form method="POST" action="{{ path('chill_main_export_saved_options_edit', {'savedExport': exportGeneration.savedExport.id, 'exportGeneration': exportGeneration.id }) }}">
|
||||||
</li>
|
<button type="submit" class="btn">
|
||||||
|
<i class="bi bi-floppy"></i> {{ 'saved_export.Update current saved export'|trans }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
{% block title %}{{ 'saved_export.Saved exports'|trans }}{% endblock %}
|
{% block title %}{{ 'saved_export.Saved exports'|trans }}{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% macro render_export_card(saved, export, export_alias, generations) %}
|
{% macro render_export_card(saved, export, export_alias, generations) %}
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
@ -30,6 +31,10 @@
|
|||||||
<p class="card-text tags">
|
<p class="card-text tags">
|
||||||
{% if app.user is same as saved.user %}<span class="badge bg-primary">{{ 'saved_export.Owner'|trans }}</span>{% endif %}
|
{% if app.user is same as saved.user %}<span class="badge bg-primary">{{ 'saved_export.Owner'|trans }}</span>{% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="card-text tags">
|
||||||
|
Partagé par <span class="badge-user">{{ saved.user|chill_entity_render_box }}</span>
|
||||||
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p class="card-text my-3">{{ saved.description|chill_markdown_to_html }}</p>
|
<p class="card-text my-3">{{ saved.description|chill_markdown_to_html }}</p>
|
||||||
</div>
|
</div>
|
||||||
@ -63,7 +68,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{# reminder: the controller already checked that the user can generate saved exports #}
|
{# 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>
|
<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>
|
||||||
<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>
|
{% 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) %}
|
{% 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>
|
<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 %}
|
{% endif %}
|
||||||
@ -82,6 +89,7 @@
|
|||||||
{{ include('@ChillMain/Export/_navbar.html.twig', {'current' : 'my'}) }}
|
{{ include('@ChillMain/Export/_navbar.html.twig', {'current' : 'my'}) }}
|
||||||
|
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
|
{{ filter|chill_render_filter_order_helper }}
|
||||||
|
|
||||||
{% if total == 0 %}
|
{% if total == 0 %}
|
||||||
<p class="chill-no-data-statement" >{{ 'saved_export.Any saved export'|trans }}</p>
|
<p class="chill-no-data-statement" >{{ 'saved_export.Any saved export'|trans }}</p>
|
||||||
|
@ -18,6 +18,13 @@
|
|||||||
|
|
||||||
{{ form_start(form) }}
|
{{ form_start(form) }}
|
||||||
{{ form_row(form.title) }}
|
{{ 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) }}
|
{{ form_row(form.description) }}
|
||||||
|
|
||||||
{% if form.share is defined %}
|
{% if form.share is defined %}
|
||||||
|
@ -15,6 +15,7 @@ use Chill\MainBundle\Entity\SavedExport;
|
|||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Export\ExportManager;
|
use Chill\MainBundle\Export\ExportManager;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
|
||||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
|
|
||||||
final class SavedExportVoter extends 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 GENERATE = 'CHILL_MAIN_EXPORT_SAVED_GENERATE';
|
||||||
|
|
||||||
|
final public const DUPLICATE = 'CHILL_MAIN_EXPORT_SAVED_DUPLICATE';
|
||||||
|
|
||||||
final public const SHARE = 'CHILL_MAIN_EXPORT_SAVED_SHARE';
|
final public const SHARE = 'CHILL_MAIN_EXPORT_SAVED_SHARE';
|
||||||
|
|
||||||
private const ALL = [
|
private const ALL = [
|
||||||
@ -32,9 +35,10 @@ final class SavedExportVoter extends Voter
|
|||||||
self::EDIT,
|
self::EDIT,
|
||||||
self::GENERATE,
|
self::GENERATE,
|
||||||
self::SHARE,
|
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
|
protected function supports($attribute, $subject): bool
|
||||||
{
|
{
|
||||||
@ -52,6 +56,7 @@ final class SavedExportVoter extends Voter
|
|||||||
|
|
||||||
return match ($attribute) {
|
return match ($attribute) {
|
||||||
self::DELETE, self::EDIT, self::SHARE => $subject->getUser() === $token->getUser(),
|
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),
|
self::GENERATE => $this->canUserGenerate($user, $subject),
|
||||||
default => throw new \UnexpectedValueException('attribute not supported: '.$attribute),
|
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 chill_main_user_group_user.user_id = u.id
|
||||||
AND jh.user_id = u.id AND tsrange(jh.startdate, jh.enddate) @> localtimestamp
|
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
|
-- only when the user's jobid is different than the user_group id
|
||||||
AND ug.userjob_id IS NOT NULL
|
-- or where the user.enabled is null
|
||||||
AND jh.job_id <> ug.userjob_id
|
AND ((ug.userjob_id IS NOT NULL AND jh.job_id <> ug.userjob_id) OR u.enabled IS NULL)
|
||||||
SQL;
|
SQL;
|
||||||
|
|
||||||
$result = $connection->executeQuery($sql);
|
$result = $connection->executeQuery($sql);
|
||||||
@ -103,7 +103,8 @@ final readonly class UserGroupRelatedToUserJobSync implements UserGroupRelatedTo
|
|||||||
SELECT cmug.id, jh.user_id
|
SELECT cmug.id, jh.user_id
|
||||||
FROM chill_main_user_group cmug
|
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
|
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
|
ON CONFLICT DO NOTHING
|
||||||
SQL;
|
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),
|
$logger ?? self::getContainer()->get(LoggerInterface::class),
|
||||||
$authorizationChecker ?? self::getContainer()->get('security.authorization_checker'),
|
$authorizationChecker ?? self::getContainer()->get('security.authorization_checker'),
|
||||||
$authorizationHelper ?? self::getContainer()->get('chill.main.security.authorization.helper'),
|
$authorizationHelper ?? self::getContainer()->get('chill.main.security.authorization.helper'),
|
||||||
$tokenStorage,
|
|
||||||
$exports,
|
$exports,
|
||||||
$aggregators,
|
$aggregators,
|
||||||
$filters,
|
$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\:
|
Chill\MainBundle\Export\Helper\:
|
||||||
resource: '../../Export/Helper'
|
resource: '../../Export/Helper'
|
||||||
|
|
||||||
|
Chill\MainBundle\Export\Cronjob\:
|
||||||
|
resource: '../../Export/Cronjob'
|
||||||
|
|
||||||
Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessageHandler: ~
|
Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessageHandler: ~
|
||||||
|
|
||||||
|
Chill\MainBundle\Export\Messenger\RemoveExportGenerationMessageHandler: ~
|
||||||
|
|
||||||
Chill\MainBundle\Export\Messenger\OnExportGenerationFails: ~
|
Chill\MainBundle\Export\Messenger\OnExportGenerationFails: ~
|
||||||
|
|
||||||
Chill\MainBundle\Export\ExportFormHelper: ~
|
Chill\MainBundle\Export\ExportFormHelper: ~
|
||||||
@ -18,6 +23,8 @@ services:
|
|||||||
|
|
||||||
Chill\MainBundle\Export\ExportConfigProcessor: ~
|
Chill\MainBundle\Export\ExportConfigProcessor: ~
|
||||||
|
|
||||||
|
Chill\MainBundle\Export\ExportDescriptionHelper: ~
|
||||||
|
|
||||||
chill.main.export_element_validator:
|
chill.main.export_element_validator:
|
||||||
class: Chill\MainBundle\Validator\Constraints\Export\ExportElementConstraintValidator
|
class: Chill\MainBundle\Validator\Constraints\Export\ExportElementConstraintValidator
|
||||||
tags:
|
tags:
|
||||||
|
@ -809,6 +809,7 @@ saved_export:
|
|||||||
Duplicated: Dupliqué
|
Duplicated: Dupliqué
|
||||||
Options updated successfully: La configuration de l'export a été mise à jour
|
Options updated successfully: La configuration de l'export a été mise à jour
|
||||||
Share: Partage
|
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:
|
absence:
|
||||||
# single letter for absence
|
# single letter for absence
|
||||||
|
Loading…
x
Reference in New Issue
Block a user