mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-23 23:25:00 +00:00
Partage d'export enregistré et génération asynchrone des exports
This commit is contained in:
@@ -14,7 +14,6 @@ namespace Chill\MainBundle;
|
||||
use Chill\MainBundle\Cron\CronJobInterface;
|
||||
use Chill\MainBundle\CRUD\CompilerPass\CRUDControllerCompilerPass;
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\ACLFlagsCompilerPass;
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\ExportsCompilerPass;
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\MenuCompilerPass;
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\NotificationCounterCompilerPass;
|
||||
use Chill\MainBundle\DependencyInjection\CompilerPass\SearchableServicesCompilerPass;
|
||||
@@ -66,7 +65,6 @@ class ChillMainBundle extends Bundle
|
||||
$container->addCompilerPass(new SearchableServicesCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||
$container->addCompilerPass(new ConfigConsistencyCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||
$container->addCompilerPass(new TimelineCompilerClass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||
$container->addCompilerPass(new ExportsCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||
$container->addCompilerPass(new WidgetsCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||
$container->addCompilerPass(new NotificationCounterCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||
$container->addCompilerPass(new MenuCompilerPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||
|
@@ -11,35 +11,39 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\ExportGeneration;
|
||||
use Chill\MainBundle\Entity\SavedExport;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Export\DirectExportInterface;
|
||||
use Chill\MainBundle\Export\ExportConfigNormalizer;
|
||||
use Chill\MainBundle\Export\ExportConfigProcessor;
|
||||
use Chill\MainBundle\Export\ExportFormHelper;
|
||||
use Chill\MainBundle\Export\ExportInterface;
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
use Chill\MainBundle\Form\SavedExportType;
|
||||
use Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage;
|
||||
use Chill\MainBundle\Form\Type\Export\ExportType;
|
||||
use Chill\MainBundle\Form\Type\Export\FormatterType;
|
||||
use Chill\MainBundle\Form\Type\Export\PickCenterType;
|
||||
use Chill\MainBundle\Redis\ChillRedis;
|
||||
use Chill\MainBundle\Repository\SavedExportRepositoryInterface;
|
||||
use Chill\MainBundle\Repository\SavedExportOrExportGenerationRepository;
|
||||
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
|
||||
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* Class ExportController
|
||||
@@ -50,117 +54,23 @@ class ExportController extends AbstractController
|
||||
private readonly bool $filterStatsByCenters;
|
||||
|
||||
public function __construct(
|
||||
private readonly ChillRedis $redis,
|
||||
private readonly ExportManager $exportManager,
|
||||
private readonly FormFactoryInterface $formFactory,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly SessionInterface $session,
|
||||
private readonly TranslatorInterface $translator,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly ExportFormHelper $exportFormHelper,
|
||||
private readonly SavedExportRepositoryInterface $savedExportRepository,
|
||||
private readonly Security $security,
|
||||
ParameterBagInterface $parameterBag,
|
||||
private readonly MessageBusInterface $messageBus,
|
||||
private readonly ClockInterface $clock,
|
||||
private readonly ExportConfigNormalizer $exportConfigNormalizer,
|
||||
private readonly SavedExportOrExportGenerationRepository $savedExportOrExportGenerationRepository,
|
||||
private readonly ExportConfigProcessor $exportConfigProcessor,
|
||||
) {
|
||||
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
|
||||
}
|
||||
|
||||
#[Route(path: '/{_locale}/exports/download/{alias}', name: 'chill_main_export_download', methods: ['GET'])]
|
||||
public function downloadResultAction(Request $request, mixed $alias)
|
||||
{
|
||||
/** @var ExportManager $exportManager */
|
||||
$exportManager = $this->exportManager;
|
||||
$export = $exportManager->getExport($alias);
|
||||
$key = $request->query->get('key', null);
|
||||
$savedExport = $this->getSavedExportFromRequest($request);
|
||||
|
||||
[$dataCenters, $dataExport, $dataFormatter] = $this->rebuildData($key, $savedExport);
|
||||
|
||||
$formatterAlias = $exportManager->getFormatterAlias($dataExport['export']);
|
||||
|
||||
if (null !== $formatterAlias) {
|
||||
$formater = $exportManager->getFormatter($formatterAlias);
|
||||
} else {
|
||||
$formater = null;
|
||||
}
|
||||
|
||||
$viewVariables = [
|
||||
'alias' => $alias,
|
||||
'export' => $export,
|
||||
'export_group' => $this->getExportGroup($export),
|
||||
'saved_export' => $savedExport,
|
||||
];
|
||||
|
||||
if ($formater instanceof \Chill\MainBundle\Export\Formatter\CSVListFormatter) {
|
||||
// due to a bug in php, we add the mime type in the download view
|
||||
$viewVariables['mime_type'] = 'text/csv';
|
||||
}
|
||||
|
||||
return $this->render('@ChillMain/Export/download.html.twig', $viewVariables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a report.
|
||||
*
|
||||
* This action must work with GET queries.
|
||||
*
|
||||
* @param string $alias
|
||||
*/
|
||||
#[Route(path: '/{_locale}/exports/generate/{alias}', name: 'chill_main_export_generate', methods: ['GET'])]
|
||||
public function generateAction(Request $request, $alias): Response
|
||||
{
|
||||
/** @var ExportManager $exportManager */
|
||||
$exportManager = $this->exportManager;
|
||||
$key = $request->query->get('key', null);
|
||||
$savedExport = $this->getSavedExportFromRequest($request);
|
||||
|
||||
[$dataCenters, $dataExport, $dataFormatter] = $this->rebuildData($key, $savedExport);
|
||||
|
||||
return $exportManager->generate(
|
||||
$alias,
|
||||
$dataCenters['centers'],
|
||||
$dataExport['export'],
|
||||
null !== $dataFormatter ? $dataFormatter['formatter'] : []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \RedisException
|
||||
*/
|
||||
#[Route(path: '/{_locale}/exports/generate-from-saved/{id}', name: 'chill_main_export_generate_from_saved')]
|
||||
public function generateFromSavedExport(SavedExport $savedExport): RedirectResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted(SavedExportVoter::GENERATE, $savedExport);
|
||||
|
||||
$key = md5(uniqid((string) random_int(0, mt_getrandmax()), false));
|
||||
|
||||
$this->redis->setEx($key, 3600, \serialize($savedExport->getOptions()));
|
||||
|
||||
return $this->redirectToRoute(
|
||||
'chill_main_export_download',
|
||||
[
|
||||
'alias' => $savedExport->getExportAlias(),
|
||||
'key' => $key, 'prevent_save' => true,
|
||||
'returnPath' => $this->generateUrl('chill_main_export_saved_list_my'),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the list of available exports.
|
||||
*/
|
||||
#[Route(path: '/{_locale}/exports/', name: 'chill_main_export_index')]
|
||||
public function indexAction(): Response
|
||||
{
|
||||
$exportManager = $this->exportManager;
|
||||
|
||||
$exports = $exportManager->getExportsGrouped(true);
|
||||
|
||||
return $this->render('@ChillMain/Export/layout.html.twig', [
|
||||
'grouped_exports' => $exports,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* handle the step to build a query for an export.
|
||||
*
|
||||
@@ -197,64 +107,6 @@ class ExportController extends AbstractController
|
||||
};
|
||||
}
|
||||
|
||||
#[Route(path: '/{_locale}/export/saved/update-from-key/{id}/{key}', name: 'chill_main_export_saved_edit_options_from_key')]
|
||||
public function editSavedExportOptionsFromKey(SavedExport $savedExport, string $key): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||
$user = $this->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
$data = $this->rebuildRawData($key);
|
||||
|
||||
$savedExport
|
||||
->setOptions($data);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->redirectToRoute('chill_main_export_saved_edit', ['id' => $savedExport->getId()]);
|
||||
}
|
||||
|
||||
#[Route(path: '/{_locale}/export/save-from-key/{alias}/{key}', name: 'chill_main_export_save_from_key')]
|
||||
public function saveFromKey(string $alias, string $key, Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||
$user = $this->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
$data = $this->rebuildRawData($key);
|
||||
|
||||
$savedExport = new SavedExport();
|
||||
$savedExport
|
||||
->setOptions($data)
|
||||
->setExportAlias($alias)
|
||||
->setUser($user);
|
||||
|
||||
$form = $this->createForm(SavedExportType::class, $savedExport);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->entityManager->persist($savedExport);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $this->redirectToRoute('chill_main_export_index');
|
||||
}
|
||||
|
||||
return $this->render(
|
||||
'@ChillMain/SavedExport/new.html.twig',
|
||||
[
|
||||
'form' => $form->createView(),
|
||||
'saved_export' => $savedExport,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* create a form to show on different steps.
|
||||
*
|
||||
@@ -262,19 +114,26 @@ class ExportController extends AbstractController
|
||||
*/
|
||||
protected function createCreateFormExport(string $alias, string $step, array $data, ?SavedExport $savedExport): FormInterface
|
||||
{
|
||||
/** @var ExportManager $exportManager */
|
||||
$exportManager = $this->exportManager;
|
||||
$isGenerate = str_starts_with($step, 'generate_');
|
||||
$canEditFull = $this->security->isGranted(ChillExportVoter::COMPOSE_EXPORT);
|
||||
|
||||
if (!$canEditFull && null === $savedExport) {
|
||||
throw new AccessDeniedHttpException('The user is not allowed to edit all filter, it should edit only SavedExport');
|
||||
}
|
||||
|
||||
$options = match ($step) {
|
||||
'export', 'generate_export' => [
|
||||
'export_alias' => $alias,
|
||||
'picked_centers' => $exportManager->getPickedCenters($data['centers'] ?? []),
|
||||
'picked_centers' => $this->filterStatsByCenters ? $this->exportFormHelper->getPickedCenters($data) : [],
|
||||
'can_edit_full' => $canEditFull,
|
||||
'allowed_filters' => $canEditFull ? null : $this->exportConfigProcessor->retrieveUsedFilters($savedExport->getOptions()['filters']),
|
||||
'allowed_aggregators' => $canEditFull ? null : $this->exportConfigProcessor->retrieveUsedAggregators($savedExport->getOptions()['aggregators']),
|
||||
],
|
||||
'formatter', 'generate_formatter' => [
|
||||
'export_alias' => $alias,
|
||||
'formatter_alias' => $exportManager->getFormatterAlias($data['export']),
|
||||
'aggregator_aliases' => $exportManager->getUsedAggregatorsAliases($data['export']),
|
||||
'aggregator_aliases' => $exportManager->getUsedAggregatorsAliases($data['export']['aggregators']),
|
||||
],
|
||||
default => [
|
||||
'export_alias' => $alias,
|
||||
@@ -283,14 +142,14 @@ class ExportController extends AbstractController
|
||||
|
||||
$defaultFormData = match ($savedExport) {
|
||||
null => $this->exportFormHelper->getDefaultData($step, $exportManager->getExport($alias), $options),
|
||||
default => $this->exportFormHelper->savedExportDataToFormData($savedExport, $step, $options),
|
||||
default => $this->exportFormHelper->savedExportDataToFormData($savedExport, $step),
|
||||
};
|
||||
|
||||
$builder = $this->formFactory
|
||||
->createNamedBuilder(
|
||||
'',
|
||||
FormType::class,
|
||||
$defaultFormData,
|
||||
'centers' === $step ? ['centers' => $defaultFormData] : $defaultFormData,
|
||||
[
|
||||
'method' => $isGenerate ? Request::METHOD_GET : Request::METHOD_POST,
|
||||
'csrf_protection' => !$isGenerate,
|
||||
@@ -429,13 +288,14 @@ class ExportController extends AbstractController
|
||||
* and redirect to the `generate` action.
|
||||
*
|
||||
* The data from previous steps is removed from session.
|
||||
*
|
||||
* @param string $alias
|
||||
*
|
||||
* @return RedirectResponse
|
||||
*/
|
||||
private function forwardToGenerate(Request $request, DirectExportInterface|ExportInterface $export, $alias, ?SavedExport $savedExport)
|
||||
private function forwardToGenerate(Request $request, DirectExportInterface|ExportInterface $export, $alias, ?SavedExport $savedExport): Response
|
||||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
throw new AccessDeniedHttpException('only regular users can generate export');
|
||||
}
|
||||
$dataCenters = $this->session->get('centers_step_raw', null);
|
||||
$dataFormatter = $this->session->get('formatter_step_raw', null);
|
||||
$dataExport = $this->session->get('export_step_raw', null);
|
||||
@@ -448,60 +308,82 @@ class ExportController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
$parameters = [
|
||||
'formatter' => $dataFormatter ?? [],
|
||||
'export' => $dataExport ?? [],
|
||||
'centers' => $dataCenters ?? [],
|
||||
'alias' => $alias,
|
||||
];
|
||||
unset($parameters['_token']);
|
||||
$key = md5(uniqid((string) random_int(0, mt_getrandmax()), false));
|
||||
$dataToNormalize = $this->buildExportDataForNormalization(
|
||||
$alias,
|
||||
$dataCenters,
|
||||
$dataExport,
|
||||
$dataFormatter,
|
||||
$savedExport,
|
||||
);
|
||||
|
||||
$this->redis->setEx($key, 3600, \serialize($parameters));
|
||||
$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));
|
||||
|
||||
// remove data from session
|
||||
$this->session->remove('centers_step_raw');
|
||||
$this->session->remove('export_step_raw');
|
||||
$this->session->remove('export_step');
|
||||
$this->session->remove('formatter_step_raw');
|
||||
$this->session->remove('formatter_step');
|
||||
|
||||
return $this->redirectToRoute('chill_main_export_download', [
|
||||
'key' => $key,
|
||||
'alias' => $alias,
|
||||
'from_saved' => $savedExport?->getId(),
|
||||
]);
|
||||
return $this->redirectToRoute('chill_main_export-generation_wait', ['id' => $exportGeneration->getId()]);
|
||||
}
|
||||
|
||||
private function rebuildData($key, ?SavedExport $savedExport)
|
||||
/**
|
||||
* Build the export form data into a way suitable for normalization.
|
||||
*
|
||||
* @param string $alias the export alias
|
||||
* @param array $dataCenters Raw data from center step
|
||||
* @param array $dataExport Raw data from export step
|
||||
* @param array $dataFormatter Raw data from formatter step
|
||||
*/
|
||||
private function buildExportDataForNormalization(string $alias, ?array $dataCenters, array $dataExport, array $dataFormatter, ?SavedExport $savedExport): array
|
||||
{
|
||||
$rawData = $this->rebuildRawData($key);
|
||||
|
||||
$alias = $rawData['alias'];
|
||||
|
||||
if ($this->filterStatsByCenters) {
|
||||
$formCenters = $this->createCreateFormExport($alias, 'generate_centers', [], $savedExport);
|
||||
$formCenters->submit($rawData['centers']);
|
||||
$dataCenters = $formCenters->getData();
|
||||
$formCenters = $this->createCreateFormExport($alias, 'generate_centers', [], null);
|
||||
$formCenters->submit($dataCenters);
|
||||
$dataAsCollection = $formCenters->getData()['centers'];
|
||||
$centers = $dataAsCollection['centers'];
|
||||
$regroupments = $dataAsCollection['regroupments'] ?? [];
|
||||
$dataCenters = [
|
||||
'centers' => $centers instanceof Collection ? $centers->toArray() : $centers,
|
||||
'regroupments' => $regroupments instanceof Collection ? $regroupments->toArray() : $regroupments,
|
||||
];
|
||||
} else {
|
||||
$dataCenters = ['centers' => []];
|
||||
$dataCenters = ['centers' => [], 'regroupments' => []];
|
||||
}
|
||||
|
||||
$formExport = $this->createCreateFormExport($alias, 'generate_export', $dataCenters, $savedExport);
|
||||
$formExport->submit($rawData['export']);
|
||||
$formExport->submit($dataExport);
|
||||
$dataExport = $formExport->getData();
|
||||
|
||||
if (\count($rawData['formatter']) > 0) {
|
||||
if (\count($dataFormatter) > 0) {
|
||||
$formFormatter = $this->createCreateFormExport(
|
||||
$alias,
|
||||
'generate_formatter',
|
||||
$dataExport,
|
||||
$savedExport
|
||||
);
|
||||
$formFormatter->submit($rawData['formatter']);
|
||||
$formFormatter->submit($dataFormatter);
|
||||
$dataFormatter = $formFormatter->getData();
|
||||
}
|
||||
|
||||
return [$dataCenters, $dataExport, $dataFormatter ?? null];
|
||||
return [
|
||||
'centers' => ['centers' => $dataCenters['centers'], 'regroupments' => $dataCenters['regroupments']],
|
||||
'export' => $dataExport['export']['export'] ?? [],
|
||||
'filters' => $dataExport['export']['filters'] ?? [],
|
||||
'aggregators' => $dataExport['export']['aggregators'] ?? [],
|
||||
'pick_formatter' => $dataExport['export']['pick_formatter']['alias'],
|
||||
'formatter' => $dataFormatter['formatter'] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -509,7 +391,7 @@ class ExportController extends AbstractController
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
private function selectCentersStep(Request $request, DirectExportInterface|ExportInterface $export, $alias, ?SavedExport $savedExport = null)
|
||||
private function selectCentersStep(Request $request, DirectExportInterface|ExportInterface $export, $alias, ExportGeneration|SavedExport|null $savedExport = null)
|
||||
{
|
||||
if (!$this->filterStatsByCenters) {
|
||||
return $this->redirectToRoute('chill_main_export_new', [
|
||||
@@ -522,7 +404,12 @@ class ExportController extends AbstractController
|
||||
/** @var ExportManager $exportManager */
|
||||
$exportManager = $this->exportManager;
|
||||
|
||||
$form = $this->createCreateFormExport($alias, 'centers', [], $savedExport);
|
||||
$form = $this->createCreateFormExport(
|
||||
$alias,
|
||||
'centers',
|
||||
$this->exportFormHelper->getDefaultData('centers', $export, []),
|
||||
$savedExport
|
||||
);
|
||||
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
$form->handleRequest($request);
|
||||
@@ -538,7 +425,7 @@ class ExportController extends AbstractController
|
||||
false === $exportManager->isGrantedForElement(
|
||||
$export,
|
||||
null,
|
||||
$exportManager->getPickedCenters($data['centers'])
|
||||
$this->exportFormHelper->getPickedCenters($data['centers']),
|
||||
)
|
||||
) {
|
||||
throw $this->createAccessDeniedException('you do not have access to this export for those centers');
|
||||
@@ -548,7 +435,7 @@ class ExportController extends AbstractController
|
||||
'centers_step_raw',
|
||||
$request->request->all()
|
||||
);
|
||||
$this->session->set('centers_step', $data);
|
||||
$this->session->set('centers_step', $data['centers']);
|
||||
|
||||
return $this->redirectToRoute('chill_main_export_new', [
|
||||
'step' => $this->getNextStep('centers', $export),
|
||||
@@ -632,43 +519,15 @@ class ExportController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
private function rebuildRawData(?string $key): array
|
||||
{
|
||||
if (null === $key) {
|
||||
throw $this->createNotFoundException('key does not exists');
|
||||
}
|
||||
|
||||
if (1 !== $this->redis->exists($key)) {
|
||||
$this->addFlash('error', $this->translator->trans('This report is not available any more'));
|
||||
|
||||
throw $this->createNotFoundException('key does not exists');
|
||||
}
|
||||
|
||||
$serialized = $this->redis->get($key);
|
||||
|
||||
if (false === $serialized) {
|
||||
throw new \LogicException('the key could not be reached from redis');
|
||||
}
|
||||
|
||||
$rawData = \unserialize($serialized);
|
||||
|
||||
$this->logger->notice('[export] choices for an export unserialized', [
|
||||
'key' => $key,
|
||||
'rawData' => json_encode($rawData, JSON_THROW_ON_ERROR),
|
||||
]);
|
||||
|
||||
return $rawData;
|
||||
}
|
||||
|
||||
private function getSavedExportFromRequest(Request $request): ?SavedExport
|
||||
private function getSavedExportFromRequest(Request $request): SavedExport|ExportGeneration|null
|
||||
{
|
||||
$savedExport = match ($savedExportId = $request->query->get('from_saved', '')) {
|
||||
'' => null,
|
||||
default => $this->savedExportRepository->find($savedExportId),
|
||||
default => $this->savedExportOrExportGenerationRepository->findById($savedExportId),
|
||||
};
|
||||
|
||||
if (null !== $savedExport && !$this->security->isGranted(SavedExportVoter::EDIT, $savedExport)) {
|
||||
throw new AccessDeniedHttpException('saved export edition not allowed');
|
||||
if (null !== $savedExport && !$this->security->isGranted(SavedExportVoter::GENERATE, $savedExport)) {
|
||||
throw new AccessDeniedHttpException('saved export generation not allowed');
|
||||
}
|
||||
|
||||
return $savedExport;
|
||||
|
@@ -0,0 +1,64 @@
|
||||
<?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\ExportGeneration;
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
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\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
final readonly class ExportGenerationController
|
||||
{
|
||||
public function __construct(
|
||||
private Security $security,
|
||||
private Environment $twig,
|
||||
private SerializerInterface $serializer,
|
||||
private ExportManager $exportManager,
|
||||
) {}
|
||||
|
||||
#[Route('/{_locale}/main/export-generation/{id}/wait', methods: ['GET'], name: 'chill_main_export-generation_wait')]
|
||||
public function wait(ExportGeneration $exportGeneration): Response
|
||||
{
|
||||
if (!$this->security->isGranted('ROLE_USER')) {
|
||||
throw new AccessDeniedHttpException('Only users can download an export');
|
||||
}
|
||||
|
||||
$export = $this->exportManager->getExport($exportGeneration->getExportAlias());
|
||||
|
||||
return new Response(
|
||||
$this->twig->render('@ChillMain/ExportGeneration/wait.html.twig', ['exportGeneration' => $exportGeneration, 'export' => $export]),
|
||||
);
|
||||
}
|
||||
|
||||
#[Route('/api/1.0/main/export-generation/{id}/object', methods: ['GET'])]
|
||||
public function objectStatus(ExportGeneration $exportGeneration): JsonResponse
|
||||
{
|
||||
if (!$this->security->isGranted('ROLE_USER')) {
|
||||
throw new AccessDeniedHttpException('Only users can download an export');
|
||||
}
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize(
|
||||
$exportGeneration,
|
||||
'json',
|
||||
[AbstractNormalizer::GROUPS => ['read']],
|
||||
),
|
||||
json: true,
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
<?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\ExportGeneration;
|
||||
use Chill\MainBundle\Entity\SavedExport;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Export\Messenger\ExportRequestGenerationMessage;
|
||||
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
class ExportGenerationCreateFromSavedExportController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly MessageBusInterface $messageBus,
|
||||
private readonly ClockInterface $clock,
|
||||
private readonly SerializerInterface $serializer,
|
||||
) {}
|
||||
|
||||
#[Route('/api/1.0/main/export/export-generation/create-from-saved-export/{id}', methods: ['POST'])]
|
||||
public function __invoke(SavedExport $export): JsonResponse
|
||||
{
|
||||
if (!$this->security->isGranted(SavedExportVoter::GENERATE, $export)) {
|
||||
throw new AccessDeniedHttpException('Not allowed to generate an export from this saved export');
|
||||
}
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
throw new AccessDeniedHttpException('Only users can create exports');
|
||||
}
|
||||
|
||||
$exportGeneration = ExportGeneration::fromSavedExport($export, $this->clock->now()->add(new \DateInterval('P6M')));
|
||||
|
||||
$this->entityManager->persist($exportGeneration);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->messageBus->dispatch(new ExportRequestGenerationMessage($exportGeneration, $user));
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize($exportGeneration, 'json', ['groups' => ['read']]),
|
||||
json: true,
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
<?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\User;
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
use Chill\MainBundle\Repository\ExportGenerationRepository;
|
||||
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Twig\Environment;
|
||||
|
||||
final readonly class ExportIndexController
|
||||
{
|
||||
public function __construct(
|
||||
private ExportManager $exportManager,
|
||||
private Environment $twig,
|
||||
private ExportGenerationRepository $exportGenerationRepository,
|
||||
private Security $security,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Render the list of available exports.
|
||||
*/
|
||||
#[Route(path: '/{_locale}/exports/', name: 'chill_main_export_index')]
|
||||
public function indexAction(ExportController $exportController): Response
|
||||
{
|
||||
$user = $this->security->getUser();
|
||||
if (!$user instanceof User) {
|
||||
throw new AccessDeniedHttpException('Only regular user can see this page');
|
||||
}
|
||||
|
||||
if (!$this->security->isGranted(ChillExportVoter::COMPOSE_EXPORT)) {
|
||||
throw new AccessDeniedHttpException(sprintf('Require the %s role', ChillExportVoter::COMPOSE_EXPORT));
|
||||
}
|
||||
|
||||
$exports = $this->exportManager->getExportsGrouped(true);
|
||||
|
||||
$lastExecutions = [];
|
||||
foreach ($this->exportManager->getExports() as $alias => $export) {
|
||||
$lastExecutions[$alias] = $this->exportGenerationRepository->findExportGenerationByAliasAndUser($alias, $user, 5);
|
||||
}
|
||||
|
||||
return new Response(
|
||||
$this->twig->render('@ChillMain/Export/layout.html.twig', [
|
||||
'grouped_exports' => $exports,
|
||||
'last_executions' => $lastExecutions,
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
@@ -11,13 +11,13 @@ declare(strict_types=1);
|
||||
|
||||
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\SavedExportRepositoryInterface;
|
||||
use Chill\MainBundle\Security\Authorization\ExportGenerationVoter;
|
||||
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
@@ -25,16 +25,28 @@ use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class SavedExportController
|
||||
final readonly class SavedExportController
|
||||
{
|
||||
public function __construct(private readonly \Twig\Environment $templating, private readonly EntityManagerInterface $entityManager, private readonly ExportManager $exportManager, private readonly FormFactoryInterface $formFactory, private readonly SavedExportRepositoryInterface $savedExportRepository, private readonly Security $security, private readonly SessionInterface $session, private readonly TranslatorInterface $translator, private readonly UrlGeneratorInterface $urlGenerator) {}
|
||||
public function __construct(
|
||||
private \Twig\Environment $templating,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private ExportManager $exportManager,
|
||||
private FormFactoryInterface $formFactory,
|
||||
private Security $security,
|
||||
private TranslatorInterface $translator,
|
||||
private UrlGeneratorInterface $urlGenerator,
|
||||
private ExportDescriptionHelper $exportDescriptionHelper,
|
||||
) {}
|
||||
|
||||
#[Route(path: '/{_locale}/exports/saved/{id}/delete', name: 'chill_main_export_saved_delete')]
|
||||
public function delete(SavedExport $savedExport, Request $request): Response
|
||||
@@ -51,7 +63,10 @@ class SavedExportController
|
||||
$this->entityManager->remove($savedExport);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->session->getFlashBag()->add('success', $this->translator->trans('saved_export.Export is deleted'));
|
||||
$session = $request->getSession();
|
||||
if ($session instanceof Session) {
|
||||
$session->getFlashBag()->add('success', new TranslatableMessage('saved_export.Export is deleted'));
|
||||
}
|
||||
|
||||
return new RedirectResponse(
|
||||
$this->urlGenerator->generate('chill_main_export_saved_list_my')
|
||||
@@ -69,6 +84,105 @@ class SavedExportController
|
||||
);
|
||||
}
|
||||
|
||||
#[Route(path: '/exports/saved/create-from-export-generation/{id}/new', name: 'chill_main_export_saved_create_from_export_generation')]
|
||||
public function createFromExportGeneration(ExportGeneration $exportGeneration, Request $request): Response
|
||||
{
|
||||
if (!$this->security->isGranted(ExportGenerationVoter::VIEW, $exportGeneration)) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
if (!$user instanceof User) {
|
||||
throw new AccessDeniedHttpException('only regular user can create a saved export');
|
||||
}
|
||||
|
||||
$export = $this->exportManager->getExport($exportGeneration->getExportAlias());
|
||||
$title = $export->getTitle() instanceof TranslatableInterface ? $export->getTitle()->trans($this->translator) :
|
||||
$this->translator->trans($export->getTitle());
|
||||
|
||||
$savedExport = new SavedExport();
|
||||
$savedExport
|
||||
->setExportAlias($exportGeneration->getExportAlias())
|
||||
->setUser($user)
|
||||
->setOptions($exportGeneration->getOptions())
|
||||
->setTitle(
|
||||
$request->query->has('title') ? $request->query->get('title') : $title
|
||||
);
|
||||
|
||||
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
|
||||
{
|
||||
$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())
|
||||
->setUser($user)
|
||||
->setOptions($previousSavedExport->getOptions())
|
||||
->setDescription($previousSavedExport->getDescription())
|
||||
->setTitle(
|
||||
$request->query->has('title') ?
|
||||
$request->query->get('title') :
|
||||
$previousSavedExport->getTitle().' ('.$this->translator->trans('saved_export.Duplicated').' '.(new \DateTimeImmutable('now'))->format('d-m-Y H:i:s').')'
|
||||
);
|
||||
|
||||
return $this->handleEdit($savedExport, $request);
|
||||
|
||||
}
|
||||
|
||||
private function handleEdit(SavedExport $savedExport, Request $request, bool $showWarningAutoGeneratedDescription = false): Response
|
||||
{
|
||||
$form = $this->formFactory->create(SavedExportType::class, $savedExport);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->entityManager->persist($savedExport);
|
||||
$this->entityManager->flush();
|
||||
|
||||
if (($session = $request->getSession()) instanceof Session) {
|
||||
$session->getFlashBag()->add('success', new TranslatableMessage('saved_export.Saved export is saved!'));
|
||||
}
|
||||
|
||||
return new RedirectResponse(
|
||||
$this->urlGenerator->generate('chill_main_export_saved_list_my'),
|
||||
);
|
||||
}
|
||||
|
||||
return new Response(
|
||||
$this->templating->render(
|
||||
'@ChillMain/SavedExport/new.html.twig',
|
||||
[
|
||||
'form' => $form->createView(),
|
||||
'showWarningAutoGeneratedDescription' => $showWarningAutoGeneratedDescription,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[Route(path: '/{_locale}/exports/saved/{id}/edit', name: 'chill_main_export_saved_edit')]
|
||||
public function edit(SavedExport $savedExport, Request $request): Response
|
||||
{
|
||||
@@ -83,10 +197,12 @@ class SavedExportController
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->session->getFlashBag()->add('success', $this->translator->trans('saved_export.Saved export is saved!'));
|
||||
if (($session = $request->getSession()) instanceof Session) {
|
||||
$session->getFlashBag()->add('success', new TranslatableMessage('saved_export.Saved export is saved!'));
|
||||
}
|
||||
|
||||
return new RedirectResponse(
|
||||
$this->urlGenerator->generate('chill_main_export_saved_list_my')
|
||||
$this->urlGenerator->generate('chill_main_export_saved_list_my'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,45 +211,37 @@ class SavedExportController
|
||||
'@ChillMain/SavedExport/edit.html.twig',
|
||||
[
|
||||
'form' => $form->createView(),
|
||||
]
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[Route(path: '/{_locale}/exports/saved/my', name: 'chill_main_export_saved_list_my')]
|
||||
public function list(): Response
|
||||
#[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
|
||||
{
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$this->security->isGranted('ROLE_USER') || !$user instanceof User) {
|
||||
throw new AccessDeniedHttpException();
|
||||
if (!$this->security->isGranted(SavedExportVoter::DUPLICATE, $savedExport)) {
|
||||
throw new AccessDeniedHttpException('You are not allowed to access this saved export');
|
||||
}
|
||||
|
||||
$exports = $this->savedExportRepository->findByUser($user, ['title' => 'ASC']);
|
||||
|
||||
// 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];
|
||||
if (!$this->security->isGranted(ExportGenerationVoter::VIEW, $exportGeneration)) {
|
||||
throw new AccessDeniedHttpException('You are not allowed to access this export generation');
|
||||
}
|
||||
|
||||
ksort($exportsGrouped);
|
||||
if ($savedExport->getExportAlias() !== $exportGeneration->getExportAlias()) {
|
||||
throw new UnprocessableEntityHttpException('export alias does not match');
|
||||
}
|
||||
|
||||
return new Response(
|
||||
$this->templating->render(
|
||||
'@ChillMain/SavedExport/index.html.twig',
|
||||
[
|
||||
'grouped_exports' => $exportsGrouped,
|
||||
'total' => \count($exports),
|
||||
]
|
||||
)
|
||||
$savedExport->setOptions($exportGeneration->getOptions());
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$session = $request->getSession();
|
||||
if ($session instanceof Session) {
|
||||
$session->getFlashBag()->add('success', new TranslatableMessage('saved_export.Options updated successfully'));
|
||||
}
|
||||
|
||||
return new RedirectResponse(
|
||||
$this->urlGenerator->generate('chill_main_export_saved_edit', ['id' => $savedExport->getId()]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -11,7 +11,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\DataFixtures\ORM;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\GroupCenter;
|
||||
use Chill\MainBundle\Entity\PermissionsGroup;
|
||||
use Chill\MainBundle\Entity\RoleScope;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
@@ -62,6 +65,15 @@ class LoadUsers extends AbstractFixture implements ContainerAwareInterface, Orde
|
||||
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$roleScope = new RoleScope();
|
||||
$roleScope->setRole('CHILL_MAIN_COMPOSE_EXPORT');
|
||||
$permissionGroup = new PermissionsGroup();
|
||||
$permissionGroup->setName('export');
|
||||
$permissionGroup->addRoleScope($roleScope);
|
||||
|
||||
$manager->persist($roleScope);
|
||||
$manager->persist($permissionGroup);
|
||||
|
||||
foreach (self::$refs as $username => $params) {
|
||||
$user = new User();
|
||||
|
||||
@@ -81,7 +93,14 @@ class LoadUsers extends AbstractFixture implements ContainerAwareInterface, Orde
|
||||
->setEmail(sprintf('%s@chill.social', \str_replace(' ', '', (string) $username)));
|
||||
|
||||
foreach ($params['groupCenterRefs'] as $groupCenterRef) {
|
||||
$user->addGroupCenter($this->getReference($groupCenterRef, GroupCenter::class));
|
||||
$user->addGroupCenter($gc = $this->getReference($groupCenterRef, GroupCenter::class));
|
||||
|
||||
$exportGroupCenter = new GroupCenter();
|
||||
$exportGroupCenter->setPermissionsGroup($permissionGroup);
|
||||
$exportGroupCenter->setCenter($gc->getCenter());
|
||||
$manager->persist($exportGroupCenter);
|
||||
|
||||
$user->addGroupCenter($exportGroupCenter);
|
||||
}
|
||||
|
||||
echo 'Creating user '.$username."... \n";
|
||||
|
@@ -78,6 +78,7 @@ use Chill\MainBundle\Form\RegroupmentType;
|
||||
use Chill\MainBundle\Form\UserGroupType;
|
||||
use Chill\MainBundle\Form\UserJobType;
|
||||
use Chill\MainBundle\Form\UserType;
|
||||
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
|
||||
use Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType;
|
||||
use Ramsey\Uuid\Doctrine\UuidType;
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
@@ -332,6 +333,9 @@ class ChillMainExtension extends Extension implements
|
||||
'strategy' => 'unanimous',
|
||||
'allow_if_all_abstain' => false,
|
||||
],
|
||||
'role_hierarchy' => [
|
||||
ChillExportVoter::COMPOSE_EXPORT => ChillExportVoter::GENERATE_SAVED_EXPORT,
|
||||
],
|
||||
]);
|
||||
|
||||
// add crud api
|
||||
|
@@ -1,102 +0,0 @@
|
||||
<?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\DependencyInjection\CompilerPass;
|
||||
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
/**
|
||||
* Compiles the services tagged with :.
|
||||
*
|
||||
* - chill.export
|
||||
* - chill.export_formatter
|
||||
* - chill.export_aggregator
|
||||
* - chill.export_filter
|
||||
* - chill.export_elements_provider
|
||||
*/
|
||||
class ExportsCompilerPass implements CompilerPassInterface
|
||||
{
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
if (!$container->has(ExportManager::class)) {
|
||||
throw new \LogicException('service '.ExportManager::class.' is not defined. It is required by ExportsCompilerPass');
|
||||
}
|
||||
|
||||
$chillManagerDefinition = $container->findDefinition(
|
||||
ExportManager::class
|
||||
);
|
||||
|
||||
$this->compileFormatters($chillManagerDefinition, $container);
|
||||
$this->compileExportElementsProvider($chillManagerDefinition, $container);
|
||||
}
|
||||
|
||||
private function compileExportElementsProvider(
|
||||
Definition $chillManagerDefinition,
|
||||
ContainerBuilder $container,
|
||||
) {
|
||||
$taggedServices = $container->findTaggedServiceIds(
|
||||
'chill.export_elements_provider'
|
||||
);
|
||||
|
||||
$knownAliases = [];
|
||||
|
||||
foreach ($taggedServices as $id => $tagAttributes) {
|
||||
foreach ($tagAttributes as $attributes) {
|
||||
if (!isset($attributes['prefix'])) {
|
||||
throw new \LogicException("the 'prefix' attribute is missing in your service '{$id}' definition");
|
||||
}
|
||||
|
||||
if (array_search($attributes['prefix'], $knownAliases, true)) {
|
||||
throw new \LogicException('There is already a chill.export_elements_provider service with prefix '.$attributes['prefix'].'. Choose another prefix.');
|
||||
}
|
||||
$knownAliases[] = $attributes['prefix'];
|
||||
|
||||
$chillManagerDefinition->addMethodCall(
|
||||
'addExportElementsProvider',
|
||||
[new Reference($id), $attributes['prefix']]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function compileFormatters(
|
||||
Definition $chillManagerDefinition,
|
||||
ContainerBuilder $container,
|
||||
) {
|
||||
$taggedServices = $container->findTaggedServiceIds(
|
||||
'chill.export_formatter'
|
||||
);
|
||||
|
||||
$knownAliases = [];
|
||||
|
||||
foreach ($taggedServices as $id => $tagAttributes) {
|
||||
foreach ($tagAttributes as $attributes) {
|
||||
if (!isset($attributes['alias'])) {
|
||||
throw new \LogicException("the 'alias' attribute is missing in your service '{$id}' definition");
|
||||
}
|
||||
|
||||
if (array_search($attributes['alias'], $knownAliases, true)) {
|
||||
throw new \LogicException('There is already a chill.export_formatter service with alias '.$attributes['alias'].'. Choose another alias.');
|
||||
}
|
||||
$knownAliases[] = $attributes['alias'];
|
||||
|
||||
$chillManagerDefinition->addMethodCall(
|
||||
'addFormatter',
|
||||
[new Reference($id), $attributes['alias']]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -23,12 +23,12 @@ class Unaccent extends FunctionNode
|
||||
{
|
||||
private ?\Doctrine\ORM\Query\AST\Node $string = null;
|
||||
|
||||
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
|
||||
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker): string
|
||||
{
|
||||
return 'UNACCENT('.$this->string->dispatch($sqlWalker).')';
|
||||
}
|
||||
|
||||
public function parse(\Doctrine\ORM\Query\Parser $parser)
|
||||
public function parse(\Doctrine\ORM\Query\Parser $parser): void
|
||||
{
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER);
|
||||
$parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS);
|
||||
|
150
src/Bundle/ChillMainBundle/Entity/ExportGeneration.php
Normal file
150
src/Bundle/ChillMainBundle/Entity/ExportGeneration.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?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\Entity;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
|
||||
/**
|
||||
* Contains the single execution of an export.
|
||||
*
|
||||
* Attached to a stored object, which will contains the result of the execution. The status of the stored object
|
||||
* is the status of the export generation.
|
||||
*
|
||||
* Generated exports should be deleted after a certain time, given by the column `deletedAt`. The stored object can be
|
||||
* deleted after the export generation removal.
|
||||
*/
|
||||
#[ORM\Entity()]
|
||||
#[ORM\Table('chill_main_export_generation')]
|
||||
#[Serializer\DiscriminatorMap('type', ['export_generation' => ExportGeneration::class])]
|
||||
class ExportGeneration implements TrackCreationInterface
|
||||
{
|
||||
use TrackCreationTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'uuid', unique: true)]
|
||||
#[Serializer\Groups(['read'])]
|
||||
private UuidInterface $id;
|
||||
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: StoredObject::class, cascade: ['persist', 'refresh'])]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
#[Serializer\Groups(['read'])]
|
||||
private StoredObject $storedObject;
|
||||
|
||||
public function __construct(
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
|
||||
#[Serializer\Groups(['read'])]
|
||||
private string $exportAlias,
|
||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
|
||||
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 ExportGeneration 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, onDelete: 'SET NULL')]
|
||||
private ?SavedExport $savedExport = null,
|
||||
) {
|
||||
$this->id = Uuid::uuid4();
|
||||
$this->storedObject = new StoredObject(StoredObject::STATUS_PENDING);
|
||||
}
|
||||
|
||||
public function setDeleteAt(?\DateTimeImmutable $deleteAt): ExportGeneration
|
||||
{
|
||||
$this->deleteAt = $deleteAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDeleteAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->deleteAt;
|
||||
}
|
||||
|
||||
public function getId(): UuidInterface
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getStoredObject(): StoredObject
|
||||
{
|
||||
return $this->storedObject;
|
||||
}
|
||||
|
||||
public function getExportAlias(): string
|
||||
{
|
||||
return $this->exportAlias;
|
||||
}
|
||||
|
||||
public function getOptions(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
public function getSavedExport(): ?SavedExport
|
||||
{
|
||||
return $this->savedExport;
|
||||
}
|
||||
|
||||
#[Serializer\Groups(['read'])]
|
||||
#[Serializer\SerializedName('status')]
|
||||
public function getStatus(): string
|
||||
{
|
||||
return $this->getStoredObject()->getStatus();
|
||||
}
|
||||
|
||||
public function setSavedExport(SavedExport $savedExport): self
|
||||
{
|
||||
$this->savedExport = $savedExport;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isLinkedToSavedExport(): bool
|
||||
{
|
||||
return null !== $this->savedExport;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
@@ -50,4 +50,9 @@ class SimpleGeographicalUnitDTO
|
||||
#[Serializer\Groups(['read'])]
|
||||
public int $layerId,
|
||||
) {}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
}
|
||||
|
@@ -102,4 +102,22 @@ class Regroupment
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the given center is contained into this regroupment.
|
||||
*/
|
||||
public function containsCenter(Center $center): bool
|
||||
{
|
||||
return $this->centers->contains($center);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if at least one of the given centers is contained into this regroupment.
|
||||
*
|
||||
* @param list<Center> $centers
|
||||
*/
|
||||
public function containsAtLeastOneCenter(array $centers): bool
|
||||
{
|
||||
return array_reduce($centers, fn (bool $carry, Center $center) => $carry || $this->containsCenter($center), false);
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,9 @@ use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\Collections\ReadableCollection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
@@ -50,9 +53,25 @@ class SavedExport implements TrackCreationInterface, TrackUpdateInterface
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
private User $user;
|
||||
|
||||
/**
|
||||
* @var Collection<int, User>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: User::class)]
|
||||
#[ORM\JoinTable(name: 'chill_main_saved_export_users')]
|
||||
private Collection $sharedWithUsers;
|
||||
|
||||
/**
|
||||
* @var Collection<int, UserGroup>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: UserGroup::class)]
|
||||
#[ORM\JoinTable(name: 'chill_main_saved_export_usergroups')]
|
||||
private Collection $sharedWithGroups;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->id = Uuid::uuid4();
|
||||
$this->sharedWithUsers = new ArrayCollection();
|
||||
$this->sharedWithGroups = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
@@ -119,4 +138,71 @@ class SavedExport implements TrackCreationInterface, TrackUpdateInterface
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addShare(User|UserGroup $shareUser): SavedExport
|
||||
{
|
||||
if ($shareUser instanceof User) {
|
||||
if (!$this->sharedWithUsers->contains($shareUser)) {
|
||||
$this->sharedWithUsers->add($shareUser);
|
||||
}
|
||||
} else {
|
||||
if (!$this->sharedWithGroups->contains($shareUser)) {
|
||||
$this->sharedWithGroups->add($shareUser);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeShare(User|UserGroup $shareUser): SavedExport
|
||||
{
|
||||
if ($shareUser instanceof User) {
|
||||
$this->sharedWithUsers->removeElement($shareUser);
|
||||
} else {
|
||||
$this->sharedWithGroups->removeElement($shareUser);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ReadableCollection<int, User|UserGroup>
|
||||
*/
|
||||
public function getShare(): ReadableCollection
|
||||
{
|
||||
return new ArrayCollection([
|
||||
...$this->sharedWithUsers->toArray(),
|
||||
...$this->sharedWithGroups->toArray(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if shared with at least one user or one group.
|
||||
*/
|
||||
public function isShared(): bool
|
||||
{
|
||||
return $this->sharedWithUsers->count() > 0 || $this->sharedWithGroups->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the user is shared with either directly or through a group.
|
||||
*
|
||||
* @param User $user the user to check
|
||||
*
|
||||
* @return bool returns true if the user is shared with directly or via group, otherwise false
|
||||
*/
|
||||
public function isSharedWithUser(User $user): bool
|
||||
{
|
||||
if ($this->sharedWithUsers->contains($user)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($this->sharedWithGroups as $group) {
|
||||
if ($group->contains($user)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@@ -21,6 +21,19 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* Represents a user group entity in the system.
|
||||
*
|
||||
* This class is used for managing user groups, including their relationships
|
||||
* with users, administrative users, and additional metadata such as colors and labels.
|
||||
*
|
||||
* Groups may be configured to have mutual exclusion properties based on an
|
||||
* exclusion key. This ensures that groups sharing the same key cannot coexist
|
||||
* in certain relationship contexts.
|
||||
*
|
||||
* Groups may be related to a UserJob. In that case, a cronjob task ensure that the members of the groups are
|
||||
* automatically synced with this group. Such groups are also automatically created by the cronjob.
|
||||
*/
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'chill_main_user_group')]
|
||||
// this discriminator key is required for automated denormalization
|
||||
@@ -71,6 +84,13 @@ class UserGroup
|
||||
#[Assert\Email]
|
||||
private string $email = '';
|
||||
|
||||
/**
|
||||
* UserJob to which the group is related.
|
||||
*/
|
||||
#[ORM\ManyToOne(targetEntity: UserJob::class)]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
private ?UserJob $userJob = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->adminUsers = new ArrayCollection();
|
||||
@@ -209,6 +229,21 @@ class UserGroup
|
||||
return '' !== $this->email;
|
||||
}
|
||||
|
||||
public function hasUserJob(): bool
|
||||
{
|
||||
return null !== $this->userJob;
|
||||
}
|
||||
|
||||
public function getUserJob(): ?UserJob
|
||||
{
|
||||
return $this->userJob;
|
||||
}
|
||||
|
||||
public function setUserJob(?UserJob $userJob): void
|
||||
{
|
||||
$this->userJob = $userJob;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current object is an instance of the UserGroup class.
|
||||
*
|
||||
|
@@ -12,25 +12,42 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Export;
|
||||
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
|
||||
/**
|
||||
* Interface for Aggregators.
|
||||
*
|
||||
* Aggregators gather result of a query. Most of the time, it will add
|
||||
* a GROUP BY clause.
|
||||
*
|
||||
* @template D of array
|
||||
*/
|
||||
interface AggregatorInterface extends ModifierInterface
|
||||
{
|
||||
/**
|
||||
* Add a form to collect data from the user.
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder);
|
||||
public function buildForm(FormBuilderInterface $builder): void;
|
||||
|
||||
/**
|
||||
* Get the default data, that can be use as "data" for the form.
|
||||
*
|
||||
* @return D
|
||||
*/
|
||||
public function getFormDefaultData(): array;
|
||||
|
||||
/**
|
||||
* @param D $formData
|
||||
*/
|
||||
public function normalizeFormData(array $formData): array;
|
||||
|
||||
/**
|
||||
* @return D
|
||||
*/
|
||||
public function denormalizeFormData(array $formData, int $fromVersion): array;
|
||||
|
||||
public function getNormalizationVersion(): int;
|
||||
|
||||
/**
|
||||
* get a callable which will be able to transform the results into
|
||||
* viewable and understable string.
|
||||
@@ -74,9 +91,9 @@ interface AggregatorInterface extends ModifierInterface
|
||||
* @param string $key The column key, as added in the query
|
||||
* @param mixed[] $values The values from the result. if there are duplicates, those might be given twice. Example: array('FR', 'BE', 'CZ', 'FR', 'BE', 'FR')
|
||||
*
|
||||
* @return \Closure where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }`
|
||||
* @return callable(mixed $value): (string|int|\DateTimeInterface|TranslatableInterface) where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }`
|
||||
*/
|
||||
public function getLabels($key, array $values, mixed $data);
|
||||
public function getLabels(string $key, array $values, mixed $data): callable;
|
||||
|
||||
/**
|
||||
* give the list of keys the current export added to the queryBuilder in
|
||||
@@ -85,7 +102,9 @@ interface AggregatorInterface extends ModifierInterface
|
||||
* Example: if your query builder will contains `SELECT count(id) AS count_id ...`,
|
||||
* this function will return `array('count_id')`.
|
||||
*
|
||||
* @param mixed[] $data the data from the export's form (added by self::buildForm)
|
||||
* @param D $data the data from the export's form (added by self::buildForm)
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getQueryKeys($data);
|
||||
public function getQueryKeys(array $data): array;
|
||||
}
|
||||
|
@@ -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()];
|
||||
}
|
||||
}
|
@@ -28,8 +28,16 @@ interface DirectExportInterface extends ExportElementInterface
|
||||
|
||||
/**
|
||||
* Generate the export.
|
||||
*
|
||||
* @return FormattedExportGeneration
|
||||
*/
|
||||
public function generate(array $acl, array $data = []): Response;
|
||||
public function generate(array $acl, array $data, ExportGenerationContext $context): Response|FormattedExportGeneration;
|
||||
|
||||
public function normalizeFormData(array $formData): array;
|
||||
|
||||
public function denormalizeFormData(array $formData, int $fromVersion): array;
|
||||
|
||||
public function getNormalizationVersion(): int;
|
||||
|
||||
/**
|
||||
* get a description, which will be used in UI (and translated).
|
||||
|
@@ -0,0 +1,14 @@
|
||||
<?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\Exception;
|
||||
|
||||
class ExportGenerationException extends ExportRuntimeException {}
|
@@ -0,0 +1,14 @@
|
||||
<?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\Exception;
|
||||
|
||||
class ExportLogicException extends \LogicException {}
|
@@ -0,0 +1,14 @@
|
||||
<?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\Exception;
|
||||
|
||||
class ExportRuntimeException extends \RuntimeException {}
|
@@ -0,0 +1,20 @@
|
||||
<?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\Exception;
|
||||
|
||||
class UnauthorizedGenerationException extends ExportGenerationException
|
||||
{
|
||||
public function __construct(string $message, ?\Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, previous: $previous);
|
||||
}
|
||||
}
|
128
src/Bundle/ChillMainBundle/Export/ExportConfigNormalizer.php
Normal file
128
src/Bundle/ChillMainBundle/Export/ExportConfigNormalizer.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?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\Center;
|
||||
use Chill\MainBundle\Entity\Regroupment;
|
||||
use Chill\MainBundle\Form\Type\Export\AggregatorType;
|
||||
use Chill\MainBundle\Form\Type\Export\ExportType;
|
||||
use Chill\MainBundle\Form\Type\Export\FilterType;
|
||||
use Chill\MainBundle\Repository\CenterRepositoryInterface;
|
||||
use Chill\MainBundle\Repository\RegroupmentRepositoryInterface;
|
||||
|
||||
/**
|
||||
* @phpstan-type NormalizedData array{centers: array{centers: list<int>, regroupments: list<int>}, export: array{form: array<string, mixed>, version: int}, filters: array<string, array{enabled: boolean, form: array<string, mixed>, version: int}>, aggregators: array<string, array{enabled: boolean, form: array<string, mixed>, version: int}>, pick_formatter: string, formatter: array{form: array<string, mixed>, version: int}}
|
||||
*/
|
||||
class ExportConfigNormalizer
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ExportManager $exportManager,
|
||||
private readonly CenterRepositoryInterface $centerRepository,
|
||||
private readonly RegroupmentRepositoryInterface $regroupmentRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return NormalizedData
|
||||
*/
|
||||
public function normalizeConfig(string $exportAlias, array $formData): array
|
||||
{
|
||||
$exportData = $formData[ExportType::EXPORT_KEY];
|
||||
$export = $this->exportManager->getExport($exportAlias);
|
||||
|
||||
$serialized = [
|
||||
'export' => [
|
||||
'form' => $export->normalizeFormData($exportData),
|
||||
'version' => $export->getNormalizationVersion(),
|
||||
],
|
||||
];
|
||||
|
||||
$serialized['centers'] = [
|
||||
'centers' => array_values(array_map(static fn (Center $center) => $center->getId(), $formData['centers']['centers'] ?? [])),
|
||||
'regroupments' => array_values(array_map(static fn (Regroupment $group) => $group->getId(), $formData['centers']['regroupments'] ?? [])),
|
||||
];
|
||||
|
||||
$filtersSerialized = [];
|
||||
foreach ($formData[ExportType::FILTER_KEY] as $alias => $filterData) {
|
||||
$filter = $this->exportManager->getFilter($alias);
|
||||
$filtersSerialized[$alias][FilterType::ENABLED_FIELD] = (bool) $filterData[FilterType::ENABLED_FIELD];
|
||||
if ($filterData[FilterType::ENABLED_FIELD]) {
|
||||
$filtersSerialized[$alias]['form'] = $filter->normalizeFormData($filterData['form']);
|
||||
$filtersSerialized[$alias]['version'] = $filter->getNormalizationVersion();
|
||||
}
|
||||
}
|
||||
$serialized['filters'] = $filtersSerialized;
|
||||
|
||||
$aggregatorsSerialized = [];
|
||||
foreach ($formData[ExportType::AGGREGATOR_KEY] as $alias => $aggregatorData) {
|
||||
$aggregator = $this->exportManager->getAggregator($alias);
|
||||
$aggregatorsSerialized[$alias][FilterType::ENABLED_FIELD] = (bool) $aggregatorData[AggregatorType::ENABLED_FIELD];
|
||||
if ($aggregatorData[AggregatorType::ENABLED_FIELD]) {
|
||||
$aggregatorsSerialized[$alias]['form'] = $aggregator->normalizeFormData($aggregatorData['form']);
|
||||
$aggregatorsSerialized[$alias]['version'] = $aggregator->getNormalizationVersion();
|
||||
}
|
||||
}
|
||||
$serialized['aggregators'] = $aggregatorsSerialized;
|
||||
|
||||
$serialized['pick_formatter'] = $formData['pick_formatter'];
|
||||
$formatter = $this->exportManager->getFormatter($formData['pick_formatter']);
|
||||
$serialized['formatter']['form'] = $formatter->normalizeFormData($formData['formatter']);
|
||||
$serialized['formatter']['version'] = $formatter->getNormalizationVersion();
|
||||
|
||||
return $serialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param NormalizedData $serializedData
|
||||
* @param bool $replaceDisabledByDefaultData if true, when a filter is not enabled, the formDefaultData is set
|
||||
*/
|
||||
public function denormalizeConfig(string $exportAlias, array $serializedData, bool $replaceDisabledByDefaultData = false): array
|
||||
{
|
||||
$export = $this->exportManager->getExport($exportAlias);
|
||||
$formater = $this->exportManager->getFormatter($serializedData['pick_formatter']);
|
||||
|
||||
$filtersConfig = [];
|
||||
foreach ($serializedData['filters'] as $alias => $filterData) {
|
||||
$aggregator = $this->exportManager->getFilter($alias);
|
||||
$filtersConfig[$alias]['enabled'] = $filterData['enabled'];
|
||||
|
||||
if ($filterData['enabled']) {
|
||||
$filtersConfig[$alias]['form'] = $aggregator->denormalizeFormData($filterData['form'], $filterData['version']);
|
||||
} elseif ($replaceDisabledByDefaultData) {
|
||||
$filtersConfig[$alias]['form'] = $aggregator->getFormDefaultData();
|
||||
}
|
||||
}
|
||||
|
||||
$aggregatorsConfig = [];
|
||||
foreach ($serializedData['aggregators'] as $alias => $aggregatorData) {
|
||||
$aggregator = $this->exportManager->getAggregator($alias);
|
||||
$aggregatorsConfig[$alias]['enabled'] = $aggregatorData['enabled'];
|
||||
|
||||
if ($aggregatorData['enabled']) {
|
||||
$aggregatorsConfig[$alias]['form'] = $aggregator->denormalizeFormData($aggregatorData['form'], $aggregatorData['version']);
|
||||
} elseif ($replaceDisabledByDefaultData) {
|
||||
$aggregatorsConfig[$alias]['form'] = $aggregator->getFormDefaultData();
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'export' => $export->denormalizeFormData($serializedData['export']['form'], $serializedData['export']['version']),
|
||||
'filters' => $filtersConfig,
|
||||
'aggregators' => $aggregatorsConfig,
|
||||
'pick_formatter' => $serializedData['pick_formatter'],
|
||||
'formatter' => $formater->denormalizeFormData($serializedData['formatter']['form'], $serializedData['formatter']['version']),
|
||||
'centers' => [
|
||||
'centers' => array_values(array_filter(array_map(fn (int $id) => $this->centerRepository->find($id), $serializedData['centers']['centers']), fn ($item) => null !== $item)),
|
||||
'regroupments' => array_values(array_filter(array_map(fn (int $id) => $this->regroupmentRepository->find($id), $serializedData['centers']['regroupments']), fn ($item) => null !== $item)),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
49
src/Bundle/ChillMainBundle/Export/ExportConfigProcessor.php
Normal file
49
src/Bundle/ChillMainBundle/Export/ExportConfigProcessor.php
Normal file
@@ -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;
|
||||
|
||||
class ExportConfigProcessor
|
||||
{
|
||||
public function __construct(private readonly ExportManager $exportManager) {}
|
||||
|
||||
/**
|
||||
* @return iterable<string, AggregatorInterface>
|
||||
*/
|
||||
public function retrieveUsedAggregators(mixed $data): iterable
|
||||
{
|
||||
if (null === $data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($data as $alias => $aggregatorData) {
|
||||
if ($this->exportManager->hasAggregator($alias) && true === $aggregatorData['enabled']) {
|
||||
yield $alias => $this->exportManager->getAggregator($alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, FilterInterface>
|
||||
*/
|
||||
public function retrieveUsedFilters(mixed $data): iterable
|
||||
{
|
||||
if (null === $data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($data as $alias => $filterData) {
|
||||
if ($this->exportManager->hasFilter($alias) && true === $filterData['enabled']) {
|
||||
yield $alias => $this->exportManager->getFilter($alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
189
src/Bundle/ChillMainBundle/Export/ExportDataNormalizerTrait.php
Normal file
189
src/Bundle/ChillMainBundle/Export/ExportDataNormalizerTrait.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<?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 Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
/**
|
||||
* Provides utilities for normalizing and denormalizing data entities and dates.
|
||||
*/
|
||||
trait ExportDataNormalizerTrait
|
||||
{
|
||||
/**
|
||||
* Normalizes a Doctrine entity or a collection of entities to extract their identifiers.
|
||||
*
|
||||
* @param object|list<object>|null $entity the entity or collection of entities to normalize
|
||||
*
|
||||
* @return array|int|string Returns the identifier(s) of the entity or entities. If an array of entities is provided,
|
||||
* an array of their identifiers is returned. If a single entity is provided, its identifier
|
||||
* is returned. If null, returns an empty value.
|
||||
*/
|
||||
private function normalizeDoctrineEntity(object|array|null $entity): array|int|string
|
||||
{
|
||||
if (is_array($entity)) {
|
||||
return array_values(array_filter(array_map(static fn (object $entity) => $entity->getId(), $entity), fn ($value) => null !== $value));
|
||||
}
|
||||
if ($entity instanceof Collection) {
|
||||
return $this->normalizeDoctrineEntity($entity->toArray());
|
||||
}
|
||||
|
||||
return $entity?->getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Denormalizes a Doctrine entity by fetching it from the provided repository based on the given ID(s).
|
||||
*
|
||||
* @param list<int>|int|string $id the identifier(s) of the entity to find
|
||||
* @param ObjectRepository $repository the Doctrine repository to query
|
||||
*
|
||||
* @return object|array<object> the found entity or an array of entities if multiple IDs are provided
|
||||
*
|
||||
* @throws \UnexpectedValueException when the entity with the given ID does not exist
|
||||
*/
|
||||
private function denormalizeDoctrineEntity(array|int|string $id, ObjectRepository $repository): object|array
|
||||
{
|
||||
if (is_array($id)) {
|
||||
if ([] === $id) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $repository->findBy(['id' => $id]);
|
||||
}
|
||||
|
||||
if (null === $object = $repository->find($id)) {
|
||||
throw new \UnexpectedValueException(sprintf('Object with id "%s" does not exist.', $id));
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizer the "user or me" values.
|
||||
*
|
||||
* @param 'me'|User|iterable<'me'|User> $user
|
||||
*
|
||||
* @return int|'me'|list<'me'|int>
|
||||
*/
|
||||
private function normalizeUserOrMe(string|User|iterable $user): int|string|array
|
||||
{
|
||||
if (is_iterable($user)) {
|
||||
$users = [];
|
||||
foreach ($user as $u) {
|
||||
$users[] = $this->normalizeUserOrMe($u);
|
||||
}
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
if ('me' === $user) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
return $user->getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param 'me'|int|iterable<'me'|int> $userId
|
||||
*
|
||||
* @return 'me'|User|array|null
|
||||
*/
|
||||
private function denormalizeUserOrMe(string|int|iterable $userId, UserRepositoryInterface $userRepository): string|User|array|null
|
||||
{
|
||||
if (is_iterable($userId)) {
|
||||
$users = [];
|
||||
foreach ($userId as $id) {
|
||||
$users[] = $this->denormalizeUserOrMe($id, $userRepository);
|
||||
}
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
if ('me' === $userId) {
|
||||
return 'me';
|
||||
}
|
||||
|
||||
return $userRepository->find($userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param 'me'|User|iterable<'me'|User> $user
|
||||
*
|
||||
* @return User|list<User>
|
||||
*/
|
||||
private function userOrMe(string|User|iterable $user, ExportGenerationContext $context): User|array
|
||||
{
|
||||
if (is_iterable($user)) {
|
||||
$users = [];
|
||||
foreach ($user as $u) {
|
||||
$users[] = $this->userOrMe($u, $context);
|
||||
}
|
||||
|
||||
return array_values(
|
||||
array_filter($users, static fn (?User $user) => null !== $user)
|
||||
);
|
||||
}
|
||||
|
||||
if ('me' === $user) {
|
||||
return $context->byUser;
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a provided date into a specific string format.
|
||||
*
|
||||
* @param \DateTimeImmutable|\DateTime $date the date instance to normalize
|
||||
*
|
||||
* @return string a formatted string containing the type and formatted date
|
||||
*/
|
||||
private function normalizeDate(\DateTimeImmutable|\DateTime $date): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s,%s',
|
||||
$date instanceof \DateTimeImmutable ? 'imm1' : 'mut1',
|
||||
$date->format('d-m-Y-H:i:s.u e'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Denormalizes a string back into a DateTime instance.
|
||||
*
|
||||
* The string is expected to contain a kind selector (e.g., 'imm1' or 'mut1')
|
||||
* to determine the type of DateTime object (immutable or mutable) followed by a date format.
|
||||
*
|
||||
* @param string $date the string to be denormalized, containing the kind selector and formatted date
|
||||
*
|
||||
* @return \DateTimeImmutable|\DateTime a DateTime instance created from the given string
|
||||
*
|
||||
* @throws \UnexpectedValueException if the kind selector or date format is invalid
|
||||
*/
|
||||
private function denormalizeDate(string $date): \DateTimeImmutable|\DateTime
|
||||
{
|
||||
$format = 'd-m-Y-H:i:s.u e';
|
||||
|
||||
$denormalized = match (substr($date, 0, 4)) {
|
||||
'imm1' => \DateTimeImmutable::createFromFormat($format, substr($date, 5)),
|
||||
'mut1' => \DateTime::createFromFormat($format, substr($date, 5)),
|
||||
default => throw new \UnexpectedValueException(sprintf('Unexpected format for the kind selector: %s', substr($date, 0, 4))),
|
||||
};
|
||||
|
||||
if (false === $denormalized) {
|
||||
throw new \UnexpectedValueException(sprintf('Unexpected date format: %s', substr($date, 5)));
|
||||
}
|
||||
|
||||
return $denormalized;
|
||||
}
|
||||
}
|
@@ -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] ?? []);
|
||||
}
|
||||
}
|
@@ -11,6 +11,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Export;
|
||||
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
|
||||
/**
|
||||
* The common methods between different object used to build export (i.e. : ExportInterface,
|
||||
* FilterInterface, AggregatorInterface).
|
||||
@@ -19,8 +21,6 @@ interface ExportElementInterface
|
||||
{
|
||||
/**
|
||||
* get a title, which will be used in UI (and translated).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle();
|
||||
public function getTitle(): string|TranslatableInterface;
|
||||
}
|
||||
|
@@ -31,5 +31,5 @@ interface ExportElementValidatedInterface
|
||||
* validate the form's data and, if required, build a contraint
|
||||
* violation on the data.
|
||||
*/
|
||||
public function validateForm(mixed $data, ExecutionContextInterface $context);
|
||||
public function validateForm(mixed $data, ExecutionContextInterface $context): void;
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ namespace Chill\MainBundle\Export;
|
||||
interface ExportElementsProviderInterface
|
||||
{
|
||||
/**
|
||||
* @return ExportElementInterface[]
|
||||
* @return iterable<ExportElementInterface>
|
||||
*/
|
||||
public function getExportElements();
|
||||
public function getExportElements(): iterable;
|
||||
}
|
||||
|
@@ -11,27 +11,28 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Export;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\ExportGeneration;
|
||||
use Chill\MainBundle\Entity\SavedExport;
|
||||
use Chill\MainBundle\Form\Type\Export\ExportType;
|
||||
use Chill\MainBundle\Form\Type\Export\FilterType;
|
||||
use Chill\MainBundle\Form\Type\Export\FormatterType;
|
||||
use Chill\MainBundle\Form\Type\Export\PickCenterType;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Chill\MainBundle\Service\Regroupement\CenterRegroupementResolver;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
|
||||
final readonly class ExportFormHelper
|
||||
{
|
||||
public function __construct(
|
||||
private AuthorizationHelperForCurrentUserInterface $authorizationHelper,
|
||||
private ExportManager $exportManager,
|
||||
private FormFactoryInterface $formFactory,
|
||||
private ExportConfigNormalizer $configNormalizer,
|
||||
private CenterRegroupementResolver $centerRegroupementResolver,
|
||||
) {}
|
||||
|
||||
public function getDefaultData(string $step, DirectExportInterface|ExportInterface $export, array $options = []): array
|
||||
{
|
||||
return match ($step) {
|
||||
'centers', 'generate_centers' => ['centers' => $this->authorizationHelper->getReachableCenters($export->requiredRole())],
|
||||
'centers', 'generate_centers' => ['centers' => $this->authorizationHelper->getReachableCenters($export->requiredRole()), 'regroupments' => []],
|
||||
'export', 'generate_export' => ['export' => $this->getDefaultDataStepExport($export, $options)],
|
||||
'formatter', 'generate_formatter' => ['formatter' => $this->getDefaultDataStepFormatter($options)],
|
||||
default => throw new \LogicException('step not allowed : '.$step),
|
||||
@@ -91,80 +92,68 @@ final readonly class ExportFormHelper
|
||||
}
|
||||
|
||||
public function savedExportDataToFormData(
|
||||
SavedExport $savedExport,
|
||||
ExportGeneration|SavedExport $savedExport,
|
||||
string $step,
|
||||
array $formOptions = [],
|
||||
): array {
|
||||
return match ($step) {
|
||||
'centers', 'generate_centers' => $this->savedExportDataToFormDataStepCenter($savedExport),
|
||||
'export', 'generate_export' => $this->savedExportDataToFormDataStepExport($savedExport, $formOptions),
|
||||
'formatter', 'generate_formatter' => $this->savedExportDataToFormDataStepFormatter($savedExport, $formOptions),
|
||||
'export', 'generate_export' => $this->savedExportDataToFormDataStepExport($savedExport),
|
||||
'formatter', 'generate_formatter' => $this->savedExportDataToFormDataStepFormatter($savedExport),
|
||||
default => throw new \LogicException('this step is not allowed: '.$step),
|
||||
};
|
||||
}
|
||||
|
||||
private function savedExportDataToFormDataStepCenter(
|
||||
SavedExport $savedExport,
|
||||
ExportGeneration|SavedExport $savedExport,
|
||||
): array {
|
||||
$builder = $this->formFactory
|
||||
->createBuilder(
|
||||
FormType::class,
|
||||
[],
|
||||
[
|
||||
'csrf_protection' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add('centers', PickCenterType::class, [
|
||||
'export_alias' => $savedExport->getExportAlias(),
|
||||
]);
|
||||
$form = $builder->getForm();
|
||||
$form->submit($savedExport->getOptions()['centers']);
|
||||
|
||||
return $form->getData();
|
||||
return [
|
||||
'centers' => $this->configNormalizer->denormalizeConfig($savedExport->getExportAlias(), $savedExport->getOptions(), true)['centers'],
|
||||
];
|
||||
}
|
||||
|
||||
private function savedExportDataToFormDataStepExport(
|
||||
SavedExport $savedExport,
|
||||
array $formOptions,
|
||||
ExportGeneration|SavedExport $savedExport,
|
||||
): array {
|
||||
$builder = $this->formFactory
|
||||
->createBuilder(
|
||||
FormType::class,
|
||||
[],
|
||||
[
|
||||
'csrf_protection' => false,
|
||||
]
|
||||
);
|
||||
$data = $this->configNormalizer->denormalizeConfig($savedExport->getExportAlias(), $savedExport->getOptions(), true);
|
||||
|
||||
$builder->add('export', ExportType::class, [
|
||||
'export_alias' => $savedExport->getExportAlias(), ...$formOptions,
|
||||
]);
|
||||
$form = $builder->getForm();
|
||||
$form->submit($savedExport->getOptions()['export']);
|
||||
|
||||
return $form->getData();
|
||||
return [
|
||||
'export' => [
|
||||
'export' => $data['export'],
|
||||
'filters' => $data['filters'],
|
||||
'pick_formatter' => ['alias' => $data['pick_formatter']],
|
||||
'aggregators' => $data['aggregators'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function savedExportDataToFormDataStepFormatter(
|
||||
SavedExport $savedExport,
|
||||
array $formOptions,
|
||||
ExportGeneration|SavedExport $savedExport,
|
||||
): array {
|
||||
$builder = $this->formFactory
|
||||
->createBuilder(
|
||||
FormType::class,
|
||||
[],
|
||||
[
|
||||
'csrf_protection' => false,
|
||||
]
|
||||
);
|
||||
$data = $this->configNormalizer->denormalizeConfig($savedExport->getExportAlias(), $savedExport->getOptions(), true);
|
||||
|
||||
$builder->add('formatter', FormatterType::class, [
|
||||
'export_alias' => $savedExport->getExportAlias(), ...$formOptions,
|
||||
]);
|
||||
$form = $builder->getForm();
|
||||
$form->submit($savedExport->getOptions()['formatter']);
|
||||
return [
|
||||
'formatter' => $data['formatter'],
|
||||
];
|
||||
}
|
||||
|
||||
return $form->getData();
|
||||
/**
|
||||
* Get the Center picked by the user for this export. The data are
|
||||
* extracted from the PickCenterType data.
|
||||
*
|
||||
* @param array $data the data as given by the @see{Chill\MainBundle\Form\Type\Export\PickCenterType}
|
||||
*
|
||||
* @return list<Center>
|
||||
*/
|
||||
public function getPickedCenters(array $data): array
|
||||
{
|
||||
if (!array_key_exists('centers', $data)) {
|
||||
throw new \RuntimeException('array has not the expected shape');
|
||||
}
|
||||
|
||||
$centers = $data['centers'] instanceof Collection ? $data['centers']->toArray() : $data['centers'];
|
||||
$regroupments = ($data['regroupments'] ?? []) instanceof Collection ? $data['regroupments']->toArray() : ($data['regroupments'] ?? []);
|
||||
|
||||
return $this->centerRegroupementResolver->resolveCenters($regroupments, $centers);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,21 @@
|
||||
<?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;
|
||||
|
||||
class ExportGenerationContext
|
||||
{
|
||||
public function __construct(
|
||||
public readonly User $byUser,
|
||||
) {}
|
||||
}
|
273
src/Bundle/ChillMainBundle/Export/ExportGenerator.php
Normal file
273
src/Bundle/ChillMainBundle/Export/ExportGenerator.php
Normal file
@@ -0,0 +1,273 @@
|
||||
<?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\Center;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Export\Exception\UnauthorizedGenerationException;
|
||||
use Chill\MainBundle\Form\Type\Export\ExportType;
|
||||
use Chill\MainBundle\Repository\CenterRepositoryInterface;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
||||
use Chill\MainBundle\Service\Regroupement\CenterRegroupementResolver;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Generate a single export.
|
||||
*/
|
||||
final readonly class ExportGenerator
|
||||
{
|
||||
private bool $filterStatsByCenters;
|
||||
|
||||
public function __construct(
|
||||
private ExportManager $exportManager,
|
||||
private ExportConfigNormalizer $configNormalizer,
|
||||
private LoggerInterface $logger,
|
||||
private AuthorizationHelperInterface $authorizationHelper,
|
||||
private CenterRegroupementResolver $centerRegroupementResolver,
|
||||
private ExportConfigProcessor $exportConfigProcessor,
|
||||
ParameterBagInterface $parameterBag,
|
||||
private CenterRepositoryInterface $centerRepository,
|
||||
) {
|
||||
$this->filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center'];
|
||||
}
|
||||
|
||||
public function generate(string $exportAlias, array $configuration, ?User $byUser = null): FormattedExportGeneration
|
||||
{
|
||||
$data = $this->configNormalizer->denormalizeConfig($exportAlias, $configuration);
|
||||
$export = $this->exportManager->getExport($exportAlias);
|
||||
|
||||
$centers = $this->filterCenters($byUser, $data['centers']['centers'], $data['centers']['regroupments'], $export);
|
||||
|
||||
$context = new ExportGenerationContext($byUser);
|
||||
|
||||
if ($export instanceof DirectExportInterface) {
|
||||
$generatedExport = $export->generate(
|
||||
$this->buildCenterReachableScopes($centers),
|
||||
$data['export'],
|
||||
$context,
|
||||
);
|
||||
|
||||
if ($generatedExport instanceof Response) {
|
||||
trigger_deprecation('chill-project/chill-bundles', '3.10', 'DirectExportInterface should not return a %s instance, but a %s instance', Response::class, FormattedExportGeneration::class);
|
||||
|
||||
return new FormattedExportGeneration($generatedExport->getContent(), $generatedExport->headers->get('Content-Type'));
|
||||
}
|
||||
|
||||
return $generatedExport;
|
||||
}
|
||||
|
||||
$query = $export->initiateQuery(
|
||||
$this->retrieveUsedModifiers($data),
|
||||
$this->buildCenterReachableScopes($centers),
|
||||
$data['export'],
|
||||
$context,
|
||||
);
|
||||
|
||||
if ($query instanceof \Doctrine\ORM\NativeQuery) {
|
||||
// throw an error if the export require other modifier, which is
|
||||
// not allowed when the export return a `NativeQuery`
|
||||
if (\count($export->supportsModifiers()) > 0) {
|
||||
throw new \LogicException("The export with alias `{$exportAlias}` return ".'a `\Doctrine\ORM\NativeQuery` and supports modifiers, which is not allowed. Either the method `supportsModifiers` should return an empty array, or return a `Doctrine\ORM\QueryBuilder`');
|
||||
}
|
||||
} elseif ($query instanceof QueryBuilder) {
|
||||
// handle filters
|
||||
$this->handleFilters($query, $data[ExportType::FILTER_KEY], $context);
|
||||
|
||||
// handle aggregators
|
||||
$this->handleAggregators($query, $data[ExportType::AGGREGATOR_KEY], $context);
|
||||
|
||||
$this->logger->notice('[export] will execute this qb in export', [
|
||||
'dql' => $query->getDQL(),
|
||||
]);
|
||||
$this->logger->debug('[export] will execute this sql qb in export', [
|
||||
'sql' => $query->getQuery()->getSQL(),
|
||||
]);
|
||||
} else {
|
||||
throw new \UnexpectedValueException('The method `intiateQuery` should return a `\Doctrine\ORM\NativeQuery` or a `Doctrine\ORM\QueryBuilder` object.');
|
||||
}
|
||||
|
||||
$result = $export->getResult($query, $data['export'], $context);
|
||||
|
||||
$formatter = $this->exportManager->getFormatter($data['pick_formatter']);
|
||||
$filtersData = [];
|
||||
$aggregatorsData = [];
|
||||
|
||||
if ($query instanceof QueryBuilder) {
|
||||
foreach ($this->exportConfigProcessor->retrieveUsedAggregators($data[ExportType::AGGREGATOR_KEY]) as $alias => $aggregator) {
|
||||
$aggregatorsData[$alias] = $data[ExportType::AGGREGATOR_KEY][$alias]['form'];
|
||||
}
|
||||
foreach ($this->exportConfigProcessor->retrieveUsedFilters($data[ExportType::FILTER_KEY]) as $alias => $filter) {
|
||||
$filtersData[$alias] = $data[ExportType::FILTER_KEY][$alias]['form'];
|
||||
}
|
||||
}
|
||||
|
||||
/* @phpstan-ignore-next-line the method "generate" is not yet implemented on all formatters */
|
||||
if (method_exists($formatter, 'generate')) {
|
||||
return $formatter->generate(
|
||||
$result,
|
||||
$data['formatter'],
|
||||
$exportAlias,
|
||||
$data['export'],
|
||||
$filtersData,
|
||||
$aggregatorsData,
|
||||
$context,
|
||||
);
|
||||
}
|
||||
|
||||
trigger_deprecation('chill-project/chill-bundles', '3.10', '%s should implements the "generate" method', FormatterInterface::class);
|
||||
|
||||
/* @phpstan-ignore-next-line this is a deprecated method that we must still call */
|
||||
$generatedExport = $formatter->getResponse(
|
||||
$result,
|
||||
$data['formatter'],
|
||||
$exportAlias,
|
||||
$data['export'],
|
||||
$filtersData,
|
||||
$aggregatorsData,
|
||||
$context,
|
||||
);
|
||||
|
||||
return new FormattedExportGeneration($generatedExport->getContent(), $generatedExport->headers->get('content-type'));
|
||||
}
|
||||
|
||||
private function filterCenters(User $byUser, array $centers, array $regroupements, ExportInterface|DirectExportInterface $export): array
|
||||
{
|
||||
if (!$this->filterStatsByCenters) {
|
||||
return $this->centerRepository->findActive();
|
||||
}
|
||||
|
||||
$authorizedCenters = new ArrayCollection($this->authorizationHelper->getReachableCenters($byUser, $export->requiredRole()));
|
||||
if ($authorizedCenters->isEmpty()) {
|
||||
throw new UnauthorizedGenerationException('No authorized centers');
|
||||
}
|
||||
|
||||
$wantedCenters = $this->centerRegroupementResolver->resolveCenters($regroupements, $centers);
|
||||
|
||||
$resolvedCenters = [];
|
||||
foreach ($wantedCenters as $wantedCenter) {
|
||||
if ($authorizedCenters->contains($wantedCenter)) {
|
||||
$resolvedCenters[] = $wantedCenter;
|
||||
}
|
||||
}
|
||||
|
||||
if ([] == $resolvedCenters) {
|
||||
throw new UnauthorizedGenerationException('No common centers between wanted centers and authorized centers');
|
||||
}
|
||||
|
||||
return $resolvedCenters;
|
||||
}
|
||||
|
||||
/**
|
||||
* parse the data to retrieve the used filters and aggregators.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
private function retrieveUsedModifiers(mixed $data): array
|
||||
{
|
||||
if (null === $data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$usedTypes = array_merge(
|
||||
$this->retrieveUsedFiltersType($data[ExportType::FILTER_KEY]),
|
||||
$this->retrieveUsedAggregatorsType($data[ExportType::AGGREGATOR_KEY]),
|
||||
);
|
||||
|
||||
return array_values(array_unique($usedTypes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the filter used in this export.
|
||||
*
|
||||
* @return list<string> an array with types
|
||||
*/
|
||||
private function retrieveUsedFiltersType(mixed $data): array
|
||||
{
|
||||
if (null === $data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$usedTypes = [];
|
||||
|
||||
foreach ($this->exportConfigProcessor->retrieveUsedFilters($data) as $filter) {
|
||||
if (!\in_array($filter->applyOn(), $usedTypes, true)) {
|
||||
$usedTypes[] = $filter->applyOn();
|
||||
}
|
||||
}
|
||||
|
||||
return $usedTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function retrieveUsedAggregatorsType(mixed $data): array
|
||||
{
|
||||
if (null === $data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$usedTypes = [];
|
||||
|
||||
foreach ($this->exportConfigProcessor->retrieveUsedAggregators($data) as $alias => $aggregator) {
|
||||
if (!\in_array($aggregator->applyOn(), $usedTypes, true)) {
|
||||
$usedTypes[] = $aggregator->applyOn();
|
||||
}
|
||||
}
|
||||
|
||||
return $usedTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alter the query with selected aggregators.
|
||||
*/
|
||||
private function handleAggregators(
|
||||
QueryBuilder $qb,
|
||||
array $data,
|
||||
ExportGenerationContext $context,
|
||||
): void {
|
||||
foreach ($this->exportConfigProcessor->retrieveUsedAggregators($data) as $alias => $aggregator) {
|
||||
$formData = $data[$alias];
|
||||
$aggregator->alterQuery($qb, $formData['form'], $context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* alter the query with selected filters.
|
||||
*/
|
||||
private function handleFilters(
|
||||
QueryBuilder $qb,
|
||||
mixed $data,
|
||||
ExportGenerationContext $context,
|
||||
): void {
|
||||
foreach ($this->exportConfigProcessor->retrieveUsedFilters($data) as $alias => $filter) {
|
||||
$formData = $data[$alias];
|
||||
|
||||
$filter->alterQuery($qb, $formData['form'], $context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* build the array required for defining centers and circles in the initiate
|
||||
* queries of ExportElementsInterfaces.
|
||||
*
|
||||
* @param list<Center> $centers
|
||||
*/
|
||||
private function buildCenterReachableScopes(array $centers)
|
||||
{
|
||||
return array_map(static fn (Center $center) => ['center' => $center, 'circles' => []], $centers);
|
||||
}
|
||||
}
|
@@ -14,6 +14,7 @@ namespace Chill\MainBundle\Export;
|
||||
use Doctrine\ORM\NativeQuery;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
|
||||
/**
|
||||
* Interface for Export.
|
||||
@@ -27,6 +28,7 @@ use Symfony\Component\Form\FormBuilderInterface;
|
||||
* @example Chill\PersonBundle\Export\CountPerson an example of implementation
|
||||
*
|
||||
* @template Q of QueryBuilder|NativeQuery
|
||||
* @template D of array
|
||||
*/
|
||||
interface ExportInterface extends ExportElementInterface
|
||||
{
|
||||
@@ -94,12 +96,12 @@ interface ExportInterface extends ExportElementInterface
|
||||
* which do not need to be translated, or value already translated in
|
||||
* database. But the header must be, in every case, translated.
|
||||
*
|
||||
* @param string $key The column key, as added in the query
|
||||
* @param mixed[] $values The values from the result. if there are duplicates, those might be given twice. Example: array('FR', 'BE', 'CZ', 'FR', 'BE', 'FR')
|
||||
* @param string $key The column key, as added in the query
|
||||
* @param list<mixed> $values The values from the result. if there are duplicates, those might be given twice. Example: array('FR', 'BE', 'CZ', 'FR', 'BE', 'FR')
|
||||
*
|
||||
* @return (callable(string|int|float|'_header'|null $value): string|int|\DateTimeInterface) where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }`
|
||||
* @return (callable(string|int|float|'_header'|null $value): string|int|\DateTimeInterface|TranslatableInterface) where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }`
|
||||
*/
|
||||
public function getLabels($key, array $values, mixed $data);
|
||||
public function getLabels(string $key, array $values, mixed $data);
|
||||
|
||||
/**
|
||||
* give the list of keys the current export added to the queryBuilder in
|
||||
@@ -108,29 +110,27 @@ interface ExportInterface extends ExportElementInterface
|
||||
* Example: if your query builder will contains `SELECT count(id) AS count_id ...`,
|
||||
* this function will return `array('count_id')`.
|
||||
*
|
||||
* @param mixed[] $data the data from the export's form (added by self::buildForm)
|
||||
* @param D $data the data from the export's form (added by self::buildForm)
|
||||
*/
|
||||
public function getQueryKeys($data);
|
||||
public function getQueryKeys(array $data): array;
|
||||
|
||||
/**
|
||||
* Return the results of the query builder.
|
||||
*
|
||||
* @param Q $query
|
||||
* @param mixed[] $data the data from the export's fomr (added by self::buildForm)
|
||||
* @param Q $query
|
||||
* @param D $data the data from the export's fomr (added by self::buildForm)
|
||||
*
|
||||
* @return mixed[] an array of results
|
||||
*/
|
||||
public function getResult($query, $data);
|
||||
public function getResult(QueryBuilder|NativeQuery $query, array $data, ExportGenerationContext $context): array;
|
||||
|
||||
/**
|
||||
* Return the Export's type. This will inform _on what_ export will apply.
|
||||
* Most of the type, it will be a string which references an entity.
|
||||
*
|
||||
* Example of types : Chill\PersonBundle\Export\Declarations::PERSON_TYPE
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType();
|
||||
public function getType(): string;
|
||||
|
||||
/**
|
||||
* The initial query, which will be modified by ModifiersInterface
|
||||
@@ -147,7 +147,21 @@ interface ExportInterface extends ExportElementInterface
|
||||
*
|
||||
* @return Q the query to execute
|
||||
*/
|
||||
public function initiateQuery(array $requiredModifiers, array $acl, array $data = []);
|
||||
public function initiateQuery(array $requiredModifiers, array $acl, array $data, ExportGenerationContext $context): QueryBuilder|NativeQuery;
|
||||
|
||||
/**
|
||||
* @param D $formData
|
||||
*/
|
||||
public function normalizeFormData(array $formData): array;
|
||||
|
||||
/**
|
||||
* @param array $formData the normalized data
|
||||
*
|
||||
* @return D
|
||||
*/
|
||||
public function denormalizeFormData(array $formData, int $fromVersion): array;
|
||||
|
||||
public function getNormalizationVersion(): int;
|
||||
|
||||
/**
|
||||
* Return the required Role to execute the Export.
|
||||
|
@@ -11,13 +11,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Export;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Form\Type\Export\ExportType;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
@@ -63,14 +59,13 @@ class ExportManager
|
||||
iterable $exports,
|
||||
iterable $aggregators,
|
||||
iterable $filters,
|
||||
// iterable $formatters,
|
||||
iterable $formatters,
|
||||
// iterable $exportElementProvider
|
||||
) {
|
||||
$this->exports = iterator_to_array($exports);
|
||||
$this->aggregators = iterator_to_array($aggregators);
|
||||
$this->filters = iterator_to_array($filters);
|
||||
// NOTE: PHP crashes on the next line (exit error code 11). This is desactivated until further investigation
|
||||
// $this->formatters = iterator_to_array($formatters);
|
||||
$this->formatters = iterator_to_array($formatters);
|
||||
|
||||
// foreach ($exportElementProvider as $prefix => $provider) {
|
||||
// $this->addExportElementsProvider($provider, $prefix);
|
||||
@@ -102,7 +97,7 @@ class ExportManager
|
||||
\in_array($filter->applyOn(), $export->supportsModifiers(), true)
|
||||
&& $this->isGrantedForElement($filter, $export, $centers)
|
||||
) {
|
||||
$filters[$alias] = $filter;
|
||||
$filters[$alias] = $this->getFilter($alias);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,25 +131,6 @@ class ExportManager
|
||||
return $aggregators;
|
||||
}
|
||||
|
||||
public function addExportElementsProvider(ExportElementsProviderInterface $provider, string $prefix): void
|
||||
{
|
||||
foreach ($provider->getExportElements() as $suffix => $element) {
|
||||
$alias = $prefix.'_'.$suffix;
|
||||
|
||||
if ($element instanceof ExportInterface) {
|
||||
$this->exports[$alias] = $element;
|
||||
} elseif ($element instanceof FilterInterface) {
|
||||
$this->filters[$alias] = $element;
|
||||
} elseif ($element instanceof AggregatorInterface) {
|
||||
$this->aggregators[$alias] = $element;
|
||||
} elseif ($element instanceof FormatterInterface) {
|
||||
$this->addFormatter($element, $alias);
|
||||
} else {
|
||||
throw new \LogicException('This element '.$element::class.' is not an instance of export element');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* add a formatter.
|
||||
*
|
||||
@@ -165,96 +141,22 @@ class ExportManager
|
||||
$this->formatters[$alias] = $formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a response which contains the requested data.
|
||||
*/
|
||||
public function generate(string $exportAlias, array $pickedCentersData, array $data, array $formatterData): Response
|
||||
{
|
||||
$export = $this->getExport($exportAlias);
|
||||
$centers = $this->getPickedCenters($pickedCentersData);
|
||||
|
||||
if ($export instanceof DirectExportInterface) {
|
||||
return $export->generate(
|
||||
$this->buildCenterReachableScopes($centers, $export),
|
||||
$data[ExportType::EXPORT_KEY]
|
||||
);
|
||||
}
|
||||
|
||||
$query = $export->initiateQuery(
|
||||
$this->retrieveUsedModifiers($data),
|
||||
$this->buildCenterReachableScopes($centers, $export),
|
||||
$data[ExportType::EXPORT_KEY]
|
||||
);
|
||||
|
||||
if ($query instanceof \Doctrine\ORM\NativeQuery) {
|
||||
// throw an error if the export require other modifier, which is
|
||||
// not allowed when the export return a `NativeQuery`
|
||||
if (\count($export->supportsModifiers()) > 0) {
|
||||
throw new \LogicException("The export with alias `{$exportAlias}` return ".'a `\Doctrine\ORM\NativeQuery` and supports modifiers, which is not allowed. Either the method `supportsModifiers` should return an empty array, or return a `Doctrine\ORM\QueryBuilder`');
|
||||
}
|
||||
} elseif ($query instanceof QueryBuilder) {
|
||||
// handle filters
|
||||
$this->handleFilters($export, $query, $data[ExportType::FILTER_KEY], $centers);
|
||||
|
||||
// handle aggregators
|
||||
$this->handleAggregators($export, $query, $data[ExportType::AGGREGATOR_KEY], $centers);
|
||||
|
||||
$this->logger->notice('[export] will execute this qb in export', [
|
||||
'dql' => $query->getDQL(),
|
||||
]);
|
||||
} else {
|
||||
throw new \UnexpectedValueException('The method `intiateQuery` should return a `\Doctrine\ORM\NativeQuery` or a `Doctrine\ORM\QueryBuilder` object.');
|
||||
}
|
||||
|
||||
$result = $export->getResult($query, $data[ExportType::EXPORT_KEY]);
|
||||
|
||||
if (!is_iterable($result)) {
|
||||
throw new \UnexpectedValueException(sprintf('The result of the export should be an iterable, %s given', \gettype($result)));
|
||||
}
|
||||
|
||||
/** @var FormatterInterface $formatter */
|
||||
$formatter = $this->getFormatter($this->getFormatterAlias($data));
|
||||
$filtersData = [];
|
||||
$aggregatorsData = [];
|
||||
|
||||
if ($query instanceof QueryBuilder) {
|
||||
$aggregators = $this->retrieveUsedAggregators($data[ExportType::AGGREGATOR_KEY]);
|
||||
|
||||
foreach ($aggregators as $alias => $aggregator) {
|
||||
$aggregatorsData[$alias] = $data[ExportType::AGGREGATOR_KEY][$alias]['form'];
|
||||
}
|
||||
}
|
||||
|
||||
$filters = $this->retrieveUsedFilters($data[ExportType::FILTER_KEY]);
|
||||
|
||||
foreach ($filters as $alias => $filter) {
|
||||
$filtersData[$alias] = $data[ExportType::FILTER_KEY][$alias]['form'];
|
||||
}
|
||||
|
||||
return $formatter->getResponse(
|
||||
$result,
|
||||
$formatterData,
|
||||
$exportAlias,
|
||||
$data[ExportType::EXPORT_KEY],
|
||||
$filtersData,
|
||||
$aggregatorsData
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $alias
|
||||
*
|
||||
* @return AggregatorInterface
|
||||
*
|
||||
* @throws \RuntimeException if the aggregator is not known
|
||||
*/
|
||||
public function getAggregator($alias)
|
||||
public function getAggregator($alias): AggregatorInterface
|
||||
{
|
||||
if (!\array_key_exists($alias, $this->aggregators)) {
|
||||
if (null === $aggregator = $this->aggregators[$alias] ?? null) {
|
||||
throw new \RuntimeException("The aggregator with alias {$alias} is not known.");
|
||||
}
|
||||
|
||||
return $this->aggregators[$alias];
|
||||
if ($aggregator instanceof ExportManagerAwareInterface) {
|
||||
$aggregator->setExportManager($this);
|
||||
}
|
||||
|
||||
return $aggregator;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -313,10 +215,10 @@ class ExportManager
|
||||
foreach ($this->exports as $alias => $export) {
|
||||
if ($whereUserIsGranted) {
|
||||
if ($this->isGrantedForElement($export, null, null)) {
|
||||
yield $alias => $export;
|
||||
yield $alias => $this->getExport($alias);
|
||||
}
|
||||
} else {
|
||||
yield $alias => $export;
|
||||
yield $alias => $this->getExport($alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -332,9 +234,9 @@ class ExportManager
|
||||
|
||||
foreach ($this->getExports($whereUserIsGranted) as $alias => $export) {
|
||||
if ($export instanceof GroupedExportInterface) {
|
||||
$groups[$export->getGroup()][$alias] = $export;
|
||||
$groups[$export->getGroup()][$alias] = $this->getExport($alias);
|
||||
} else {
|
||||
$groups['_'][$alias] = $export;
|
||||
$groups['_'][$alias] = $this->getExport($alias);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,11 +248,25 @@ class ExportManager
|
||||
*/
|
||||
public function getFilter(string $alias): FilterInterface
|
||||
{
|
||||
if (!\array_key_exists($alias, $this->filters)) {
|
||||
if (null === $filter = $this->filters[$alias] ?? null) {
|
||||
throw new \RuntimeException("The filter with alias {$alias} is not known.");
|
||||
}
|
||||
|
||||
return $this->filters[$alias];
|
||||
if ($filter instanceof ExportManagerAwareInterface) {
|
||||
$filter->setExportManager($this);
|
||||
}
|
||||
|
||||
return $filter;
|
||||
}
|
||||
|
||||
public function hasFilter(string $alias): bool
|
||||
{
|
||||
return array_key_exists($alias, $this->filters);
|
||||
}
|
||||
|
||||
public function hasAggregator(string $alias): bool
|
||||
{
|
||||
return array_key_exists($alias, $this->aggregators);
|
||||
}
|
||||
|
||||
public function getAllFilters(): array
|
||||
@@ -358,7 +274,7 @@ class ExportManager
|
||||
$filters = [];
|
||||
|
||||
foreach ($this->filters as $alias => $filter) {
|
||||
$filters[$alias] = $filter;
|
||||
$filters[$alias] = $this->getFilter($alias);
|
||||
}
|
||||
|
||||
return $filters;
|
||||
@@ -380,11 +296,15 @@ class ExportManager
|
||||
|
||||
public function getFormatter(string $alias): FormatterInterface
|
||||
{
|
||||
if (!\array_key_exists($alias, $this->formatters)) {
|
||||
if (null === $formatter = $this->formatters[$alias] ?? null) {
|
||||
throw new \RuntimeException("The formatter with alias {$alias} is not known.");
|
||||
}
|
||||
|
||||
return $this->formatters[$alias];
|
||||
if ($formatter instanceof ExportManagerAwareInterface) {
|
||||
$formatter->setExportManager($this);
|
||||
}
|
||||
|
||||
return $formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -412,26 +332,13 @@ class ExportManager
|
||||
{
|
||||
foreach ($this->formatters as $alias => $formatter) {
|
||||
if (\in_array($formatter->getType(), $types, true)) {
|
||||
yield $alias => $formatter;
|
||||
yield $alias => $this->getFormatter($alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Center picked by the user for this export. The data are
|
||||
* extracted from the PickCenterType data.
|
||||
*
|
||||
* @param array $data the data from a PickCenterType
|
||||
*
|
||||
* @return \Chill\MainBundle\Entity\Center[] the picked center
|
||||
*/
|
||||
public function getPickedCenters(array $data): array
|
||||
{
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the aggregators typse used in the form export data.
|
||||
* get the aggregators types used in the form export data.
|
||||
*
|
||||
* @param array $data the data from the export form
|
||||
*
|
||||
@@ -439,9 +346,15 @@ class ExportManager
|
||||
*/
|
||||
public function getUsedAggregatorsAliases(array $data): array
|
||||
{
|
||||
$aggregators = $this->retrieveUsedAggregators($data[ExportType::AGGREGATOR_KEY]);
|
||||
$keys = [];
|
||||
|
||||
return array_keys(iterator_to_array($aggregators));
|
||||
foreach ($data as $alias => $aggregatorData) {
|
||||
if (true === $aggregatorData['enabled']) {
|
||||
$keys[] = $alias;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($keys));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -490,190 +403,4 @@ class ExportManager
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* build the array required for defining centers and circles in the initiate
|
||||
* queries of ExportElementsInterfaces.
|
||||
*
|
||||
* @param \Chill\MainBundle\Entity\Center[] $centers
|
||||
*/
|
||||
private function buildCenterReachableScopes(array $centers, ExportElementInterface $element)
|
||||
{
|
||||
$r = [];
|
||||
|
||||
$user = $this->tokenStorage->getToken()->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($centers as $center) {
|
||||
$r[] = [
|
||||
'center' => $center,
|
||||
'circles' => $this->authorizationHelper->getReachableScopes(
|
||||
$user,
|
||||
$element->requiredRole(),
|
||||
$center
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alter the query with selected aggregators.
|
||||
*
|
||||
* Check for acl. If an user is not authorized to see an aggregator, throw an
|
||||
* UnauthorizedException.
|
||||
*
|
||||
* @throw UnauthorizedHttpException if the user is not authorized
|
||||
*/
|
||||
private function handleAggregators(
|
||||
ExportInterface $export,
|
||||
QueryBuilder $qb,
|
||||
array $data,
|
||||
array $center,
|
||||
) {
|
||||
$aggregators = $this->retrieveUsedAggregators($data);
|
||||
|
||||
foreach ($aggregators as $alias => $aggregator) {
|
||||
if (false === $this->isGrantedForElement($aggregator, $export, $center)) {
|
||||
throw new UnauthorizedHttpException('You are not authorized to use the aggregator'.$aggregator->getTitle());
|
||||
}
|
||||
|
||||
$formData = $data[$alias];
|
||||
$aggregator->alterQuery($qb, $formData['form']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* alter the query with selected filters.
|
||||
*
|
||||
* This function check the acl.
|
||||
*
|
||||
* @param \Chill\MainBundle\Entity\Center[] $centers the picked centers
|
||||
*
|
||||
* @throw UnauthorizedHttpException if the user is not authorized
|
||||
*/
|
||||
private function handleFilters(
|
||||
ExportInterface $export,
|
||||
QueryBuilder $qb,
|
||||
mixed $data,
|
||||
array $centers,
|
||||
) {
|
||||
$filters = $this->retrieveUsedFilters($data);
|
||||
|
||||
foreach ($filters as $alias => $filter) {
|
||||
if (false === $this->isGrantedForElement($filter, $export, $centers)) {
|
||||
throw new UnauthorizedHttpException('You are not authorized to use the filter '.$filter->getTitle());
|
||||
}
|
||||
|
||||
$formData = $data[$alias];
|
||||
|
||||
$this->logger->debug('alter query by filter '.$alias, [
|
||||
'class' => self::class, 'function' => __FUNCTION__,
|
||||
]);
|
||||
$filter->alterQuery($qb, $formData['form']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, AggregatorInterface>
|
||||
*/
|
||||
private function retrieveUsedAggregators(mixed $data): iterable
|
||||
{
|
||||
if (null === $data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($data as $alias => $aggregatorData) {
|
||||
if (true === $aggregatorData['enabled']) {
|
||||
yield $alias => $this->getAggregator($alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function retrieveUsedAggregatorsType(mixed $data)
|
||||
{
|
||||
if (null === $data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$usedTypes = [];
|
||||
|
||||
foreach ($this->retrieveUsedAggregators($data) as $alias => $aggregator) {
|
||||
if (!\in_array($aggregator->applyOn(), $usedTypes, true)) {
|
||||
$usedTypes[] = $aggregator->applyOn();
|
||||
}
|
||||
}
|
||||
|
||||
return $usedTypes;
|
||||
}
|
||||
|
||||
private function retrieveUsedFilters(mixed $data): iterable
|
||||
{
|
||||
if (null === $data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($data as $alias => $filterData) {
|
||||
if (true === $filterData['enabled']) {
|
||||
yield $alias => $this->getFilter($alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the filter used in this export.
|
||||
*
|
||||
* @return array an array with types
|
||||
*/
|
||||
private function retrieveUsedFiltersType(mixed $data): iterable
|
||||
{
|
||||
if (null === $data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$usedTypes = [];
|
||||
|
||||
foreach ($data as $alias => $filterData) {
|
||||
if (true === $filterData['enabled']) {
|
||||
$filter = $this->getFilter($alias);
|
||||
|
||||
if (!\in_array($filter->applyOn(), $usedTypes, true)) {
|
||||
$usedTypes[] = $filter->applyOn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $usedTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* parse the data to retrieve the used filters and aggregators.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function retrieveUsedModifiers(mixed $data)
|
||||
{
|
||||
if (null === $data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$usedTypes = array_merge(
|
||||
$this->retrieveUsedFiltersType($data[ExportType::FILTER_KEY]),
|
||||
$this->retrieveUsedAggregatorsType($data[ExportType::AGGREGATOR_KEY])
|
||||
);
|
||||
|
||||
$this->logger->debug(
|
||||
'Required types are '.implode(', ', $usedTypes),
|
||||
['class' => self::class, 'function' => __FUNCTION__]
|
||||
);
|
||||
|
||||
return array_unique($usedTypes);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,20 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Interface which is aware of the export manager.
|
||||
*/
|
||||
interface ExportManagerAwareInterface
|
||||
{
|
||||
public function setExportManager(ExportManager $exportManager): void;
|
||||
}
|
@@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Export;
|
||||
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
|
||||
/**
|
||||
* Interface for filters.
|
||||
@@ -20,6 +21,8 @@ use Symfony\Component\Form\FormBuilderInterface;
|
||||
* it will add a `WHERE` clause on this query.
|
||||
*
|
||||
* Filters should not add column in `SELECT` clause.
|
||||
*
|
||||
* @template D of array
|
||||
*/
|
||||
interface FilterInterface extends ModifierInterface
|
||||
{
|
||||
@@ -28,16 +31,30 @@ interface FilterInterface extends ModifierInterface
|
||||
/**
|
||||
* Add a form to collect data from the user.
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder);
|
||||
public function buildForm(FormBuilderInterface $builder): void;
|
||||
|
||||
/**
|
||||
* Get the default data, that can be use as "data" for the form.
|
||||
*
|
||||
* In case of adding new parameters to a filter, you can implement a @see{DataTransformerFilterInterface} to
|
||||
* transforme the filters's data saved in an export to the desired state.
|
||||
*
|
||||
* @return D
|
||||
*/
|
||||
public function getFormDefaultData(): array;
|
||||
|
||||
/**
|
||||
* @param D $formData
|
||||
*/
|
||||
public function normalizeFormData(array $formData): array;
|
||||
|
||||
/**
|
||||
* @return D
|
||||
*/
|
||||
public function denormalizeFormData(array $formData, int $fromVersion): array;
|
||||
|
||||
public function getNormalizationVersion(): int;
|
||||
|
||||
/**
|
||||
* Describe the filtering action.
|
||||
*
|
||||
@@ -52,7 +69,7 @@ interface FilterInterface extends ModifierInterface
|
||||
* supported, later some 'html' will be added. The filter should always
|
||||
* implement the 'string' format and fallback to it if other format are used.
|
||||
*
|
||||
* If no i18n is necessery, or if the filter translate the string by himself,
|
||||
* If no i18n is necessary, or if the filter translate the string by himself,
|
||||
* this function should return a string. If the filter does not do any translation,
|
||||
* the return parameter should be an array, where
|
||||
*
|
||||
@@ -63,10 +80,9 @@ interface FilterInterface extends ModifierInterface
|
||||
*
|
||||
* Example: `array('my string with %parameter%', ['%parameter%' => 'good news'], 'mydomain', 'mylocale')`
|
||||
*
|
||||
* @param array $data
|
||||
* @param string $format the format
|
||||
* @param D $data
|
||||
*
|
||||
* @return array|string a string with the data or, if translatable, an array where first element is string, second elements is an array of arguments
|
||||
* @return array|string|TranslatableInterface a string with the data or, if translatable, an array where first element is string, second elements is an array of arguments
|
||||
*/
|
||||
public function describeAction($data, $format = 'string');
|
||||
public function describeAction(array $data, ExportGenerationContext $context): array|string|TranslatableInterface;
|
||||
}
|
||||
|
@@ -0,0 +1,20 @@
|
||||
<?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;
|
||||
|
||||
final readonly class FormattedExportGeneration
|
||||
{
|
||||
public function __construct(
|
||||
public string $content,
|
||||
public string $contentType,
|
||||
) {}
|
||||
}
|
@@ -1,440 +0,0 @@
|
||||
<?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\Formatter;
|
||||
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
use Chill\MainBundle\Export\FormatterInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* Command to get the report with curl:
|
||||
* curl --user "center a_social:password" "http://localhost:8000/fr/exports/generate/count_person?export[filters][person_gender_filter][enabled]=&export[filters][person_nationality_filter][enabled]=&export[filters][person_nationality_filter][form][nationalities]=&export[aggregators][person_nationality_aggregator][order]=1&export[aggregators][person_nationality_aggregator][form][group_by_level]=country&export[submit]=&export[_token]=RHpjHl389GrK-bd6iY5NsEqrD5UKOTHH40QKE9J1edU" --globoff.
|
||||
*
|
||||
* @deprecated this formatter is not used any more
|
||||
*/
|
||||
class CSVFormatter implements FormatterInterface
|
||||
{
|
||||
protected $aggregators;
|
||||
|
||||
protected $aggregatorsData;
|
||||
|
||||
protected $export;
|
||||
|
||||
protected $exportData;
|
||||
|
||||
/**
|
||||
* @var ExportManager
|
||||
*/
|
||||
protected $exportManager;
|
||||
|
||||
protected $filtersData;
|
||||
|
||||
protected $formatterData;
|
||||
|
||||
protected $labels;
|
||||
|
||||
protected $result;
|
||||
|
||||
public function __construct(
|
||||
protected TranslatorInterface $translator,
|
||||
ExportManager $manager,
|
||||
) {
|
||||
$this->exportManager = $manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @uses appendAggregatorForm
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, $exportAlias, array $aggregatorAliases)
|
||||
{
|
||||
$aggregators = $this->exportManager->getAggregators($aggregatorAliases);
|
||||
$nb = \count($aggregatorAliases);
|
||||
|
||||
foreach ($aggregators as $alias => $aggregator) {
|
||||
$builderAggregator = $builder->create($alias, FormType::class, [
|
||||
'label' => $aggregator->getTitle(),
|
||||
'block_name' => '_aggregator_placement_csv_formatter',
|
||||
]);
|
||||
$this->appendAggregatorForm($builderAggregator, $nb);
|
||||
$builder->add($builderAggregator);
|
||||
}
|
||||
}
|
||||
|
||||
public function getFormDefaultData(array $aggregatorAliases): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function gatherFiltersDescriptions()
|
||||
{
|
||||
$descriptions = [];
|
||||
|
||||
foreach ($this->filtersData as $key => $filterData) {
|
||||
$statement = $this->exportManager
|
||||
->getFilter($key)
|
||||
->describeAction($filterData);
|
||||
|
||||
if (null === $statement) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (\is_array($statement)) {
|
||||
$descriptions[] = $this->translator->trans(
|
||||
$statement[0],
|
||||
$statement[1],
|
||||
$statement[2] ?? null,
|
||||
$statement[3] ?? null
|
||||
);
|
||||
} else {
|
||||
$descriptions[] = $statement;
|
||||
}
|
||||
}
|
||||
|
||||
return $descriptions;
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return 'Comma separated values (CSV)';
|
||||
}
|
||||
|
||||
public function getResponse(
|
||||
$result,
|
||||
$formatterData,
|
||||
$exportAlias,
|
||||
array $exportData,
|
||||
array $filtersData,
|
||||
array $aggregatorsData,
|
||||
) {
|
||||
$this->result = $result;
|
||||
$this->orderingHeaders($formatterData);
|
||||
$this->export = $this->exportManager->getExport($exportAlias);
|
||||
$this->aggregators = iterator_to_array($this->exportManager
|
||||
->getAggregators(array_keys($aggregatorsData)));
|
||||
$this->exportData = $exportData;
|
||||
$this->aggregatorsData = $aggregatorsData;
|
||||
$this->labels = $this->gatherLabels();
|
||||
$this->filtersData = $filtersData;
|
||||
|
||||
$response = new Response();
|
||||
$response->setStatusCode(200);
|
||||
$response->headers->set('Content-Type', 'text/csv; charset=utf-8');
|
||||
// $response->headers->set('Content-Disposition','attachment; filename="export.csv"');
|
||||
|
||||
$response->setContent($this->generateContent());
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function getType()
|
||||
{
|
||||
return 'tabular';
|
||||
}
|
||||
|
||||
protected function gatherLabels()
|
||||
{
|
||||
return array_merge(
|
||||
$this->gatherLabelsFromAggregators(),
|
||||
$this->gatherLabelsFromExport()
|
||||
);
|
||||
}
|
||||
|
||||
protected function gatherLabelsFromAggregators()
|
||||
{
|
||||
$labels = [];
|
||||
/* @var $aggretator \Chill\MainBundle\Export\AggregatorInterface */
|
||||
foreach ($this->aggregators as $alias => $aggregator) {
|
||||
$keys = $aggregator->getQueryKeys($this->aggregatorsData[$alias]);
|
||||
|
||||
// gather data in an array
|
||||
foreach ($keys as $key) {
|
||||
$values = array_map(static function ($row) use ($key, $alias) {
|
||||
if (!\array_key_exists($key, $row)) {
|
||||
throw new \LogicException("the key '".$key."' is declared by the aggregator with alias '".$alias."' but is not ".'present in results');
|
||||
}
|
||||
|
||||
return $row[$key];
|
||||
}, $this->result);
|
||||
$labels[$key] = $aggregator->getLabels(
|
||||
$key,
|
||||
array_unique($values),
|
||||
$this->aggregatorsData[$alias]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $labels;
|
||||
}
|
||||
|
||||
protected function gatherLabelsFromExport()
|
||||
{
|
||||
$labels = [];
|
||||
$export = $this->export;
|
||||
$keys = $this->export->getQueryKeys($this->exportData);
|
||||
|
||||
foreach ($keys as $key) {
|
||||
$values = array_map(static function ($row) use ($key, $export) {
|
||||
if (!\array_key_exists($key, $row)) {
|
||||
throw new \LogicException("the key '".$key."' is declared by the export with title '".$export->getTitle()."' but is not ".'present in results');
|
||||
}
|
||||
|
||||
return $row[$key];
|
||||
}, $this->result);
|
||||
$labels[$key] = $this->export->getLabels(
|
||||
$key,
|
||||
array_unique($values),
|
||||
$this->exportData
|
||||
);
|
||||
}
|
||||
|
||||
return $labels;
|
||||
}
|
||||
|
||||
protected function generateContent()
|
||||
{
|
||||
$line = null;
|
||||
$rowKeysNb = \count($this->getRowHeaders());
|
||||
$columnKeysNb = \count($this->getColumnHeaders());
|
||||
$resultsKeysNb = \count($this->export->getQueryKeys($this->exportData));
|
||||
$results = $this->getOrderedResults();
|
||||
/** @var string[] $columnHeaders the column headers associations */
|
||||
$columnHeaders = [];
|
||||
/** @var string[] $data the data of the csv file */
|
||||
$contentData = [];
|
||||
$content = [];
|
||||
|
||||
// create a file pointer connected to the output stream
|
||||
$output = fopen('php://output', 'wb');
|
||||
|
||||
// title
|
||||
fputcsv($output, [$this->translator->trans($this->export->getTitle())]);
|
||||
// blank line
|
||||
fputcsv($output, ['']);
|
||||
|
||||
// add filtering description
|
||||
foreach ($this->gatherFiltersDescriptions() as $desc) {
|
||||
fputcsv($output, [$desc]);
|
||||
}
|
||||
// blank line
|
||||
fputcsv($output, ['']);
|
||||
|
||||
// iterate on result to : 1. populate row headers, 2. populate column headers, 3. add result
|
||||
foreach ($results as $row) {
|
||||
$rowHeaders = \array_slice($row, 0, $rowKeysNb);
|
||||
|
||||
// first line : we create line and adding them row headers
|
||||
if (!isset($line)) {
|
||||
$line = \array_slice($row, 0, $rowKeysNb);
|
||||
}
|
||||
|
||||
// do we have to create a new line ? if the rows are equals, continue on the line, else create a next line
|
||||
if (\array_slice($line, 0, $rowKeysNb) !== $rowHeaders) {
|
||||
$contentData[] = $line;
|
||||
$line = \array_slice($row, 0, $rowKeysNb);
|
||||
}
|
||||
|
||||
// add the column headers
|
||||
/** @var string[] $columns the column for this row */
|
||||
$columns = \array_slice($row, $rowKeysNb, $columnKeysNb);
|
||||
$columnPosition = $this->findColumnPosition($columnHeaders, $columns);
|
||||
|
||||
// fill with blank at the position given by the columnPosition + nbRowHeaders
|
||||
for ($i = 0; $i < $columnPosition; ++$i) {
|
||||
if (!isset($line[$rowKeysNb + $i])) {
|
||||
$line[$rowKeysNb + $i] = '';
|
||||
}
|
||||
}
|
||||
|
||||
$resultData = \array_slice($row, $resultsKeysNb * -1);
|
||||
|
||||
foreach ($resultData as $data) {
|
||||
$line[] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
// we add the last line
|
||||
$contentData[] = $line;
|
||||
|
||||
// column title headers
|
||||
for ($i = 0; $i < $columnKeysNb; ++$i) {
|
||||
$line = array_fill(0, $rowKeysNb, '');
|
||||
|
||||
foreach ($columnHeaders as $set) {
|
||||
$line[] = $set[$i];
|
||||
}
|
||||
|
||||
$content[] = $line;
|
||||
}
|
||||
|
||||
// row title headers
|
||||
$headerLine = [];
|
||||
|
||||
foreach ($this->getRowHeaders() as $headerKey) {
|
||||
$headerLine[] = \array_key_exists('_header', $this->labels[$headerKey]) ?
|
||||
$this->labels[$headerKey]['_header'] : '';
|
||||
}
|
||||
|
||||
foreach ($this->export->getQueryKeys($this->exportData) as $key) {
|
||||
$headerLine[] = \array_key_exists('_header', $this->labels[$key]) ?
|
||||
$this->labels[$key]['_header'] : '';
|
||||
}
|
||||
fputcsv($output, $headerLine);
|
||||
unset($headerLine); // free memory
|
||||
|
||||
// generate CSV
|
||||
foreach ($content as $line) {
|
||||
fputcsv($output, $line);
|
||||
}
|
||||
|
||||
foreach ($contentData as $line) {
|
||||
fputcsv($output, $line);
|
||||
}
|
||||
|
||||
$text = stream_get_contents($output);
|
||||
fclose($output);
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
protected function getColumnHeaders()
|
||||
{
|
||||
return $this->getPositionnalHeaders('c');
|
||||
}
|
||||
|
||||
protected function getRowHeaders()
|
||||
{
|
||||
return $this->getPositionnalHeaders('r');
|
||||
}
|
||||
|
||||
/**
|
||||
* ordering aggregators, preserving key association.
|
||||
*
|
||||
* This function do not mind about position.
|
||||
*
|
||||
* If two aggregators have the same order, the second given will be placed
|
||||
* after. This is not significant for the first ordering.
|
||||
*/
|
||||
protected function orderingHeaders(array $formatterData)
|
||||
{
|
||||
$this->formatterData = $formatterData;
|
||||
uasort(
|
||||
$this->formatterData,
|
||||
static fn (array $a, array $b): int => ($a['order'] <= $b['order'] ? -1 : 1)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* append a form line by aggregator on the formatter form.
|
||||
*
|
||||
* This form allow to choose the aggregator position (row or column) and
|
||||
* the ordering
|
||||
*
|
||||
* @param string $nbAggregators
|
||||
*/
|
||||
private function appendAggregatorForm(FormBuilderInterface $builder, $nbAggregators)
|
||||
{
|
||||
$builder->add('order', ChoiceType::class, [
|
||||
'choices' => array_combine(
|
||||
range(1, $nbAggregators),
|
||||
range(1, $nbAggregators)
|
||||
),
|
||||
'multiple' => false,
|
||||
'expanded' => false,
|
||||
]);
|
||||
|
||||
$builder->add('position', ChoiceType::class, [
|
||||
'choices' => [
|
||||
'row' => 'r',
|
||||
'column' => 'c',
|
||||
],
|
||||
'multiple' => false,
|
||||
'expanded' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
private function findColumnPosition(&$columnHeaders, $columnToFind): int
|
||||
{
|
||||
$i = 0;
|
||||
|
||||
foreach ($columnHeaders as $set) {
|
||||
if ($set === $columnToFind) {
|
||||
return $i;
|
||||
}
|
||||
++$i;
|
||||
}
|
||||
|
||||
// we didn't find it, adding the column
|
||||
$columnHeaders[] = $columnToFind;
|
||||
|
||||
return $i++;
|
||||
}
|
||||
|
||||
private function getOrderedResults()
|
||||
{
|
||||
$r = [];
|
||||
$results = $this->result;
|
||||
$labels = $this->labels;
|
||||
$rowKeys = $this->getRowHeaders();
|
||||
$columnKeys = $this->getColumnHeaders();
|
||||
$resultsKeys = $this->export->getQueryKeys($this->exportData);
|
||||
$headers = array_merge($rowKeys, $columnKeys);
|
||||
|
||||
foreach ($results as $row) {
|
||||
$line = [];
|
||||
|
||||
foreach ($headers as $key) {
|
||||
$line[] = \call_user_func($labels[$key], $row[$key]);
|
||||
}
|
||||
|
||||
// append result
|
||||
foreach ($resultsKeys as $key) {
|
||||
$line[] = \call_user_func($labels[$key], $row[$key]);
|
||||
}
|
||||
|
||||
$r[] = $line;
|
||||
}
|
||||
|
||||
array_multisort($r);
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $position may be 'c' (column) or 'r' (row)
|
||||
*
|
||||
* @return string[]
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
private function getPositionnalHeaders($position)
|
||||
{
|
||||
$headers = [];
|
||||
|
||||
foreach ($this->formatterData as $alias => $data) {
|
||||
if (!\array_key_exists($alias, $this->aggregatorsData)) {
|
||||
throw new \RuntimeException('the formatter wants to use the '."aggregator with alias {$alias}, but the export do not ".'contains data about it');
|
||||
}
|
||||
|
||||
$aggregator = $this->aggregators[$alias];
|
||||
|
||||
if ($data['position'] === $position) {
|
||||
$headers = array_merge($headers, $aggregator->getQueryKeys($this->aggregatorsData[$alias]));
|
||||
}
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
}
|
@@ -1,223 +0,0 @@
|
||||
<?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\Formatter;
|
||||
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
use Chill\MainBundle\Export\FormatterInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
// command to get the report with curl : curl --user "center a_social:password" "http://localhost:8000/fr/exports/generate/count_person?export[filters][person_gender_filter][enabled]=&export[filters][person_nationality_filter][enabled]=&export[filters][person_nationality_filter][form][nationalities]=&export[aggregators][person_nationality_aggregator][order]=1&export[aggregators][person_nationality_aggregator][form][group_by_level]=country&export[submit]=&export[_token]=RHpjHl389GrK-bd6iY5NsEqrD5UKOTHH40QKE9J1edU" --globoff
|
||||
|
||||
/**
|
||||
* Create a CSV List for the export.
|
||||
*/
|
||||
class CSVListFormatter implements FormatterInterface
|
||||
{
|
||||
protected $exportAlias;
|
||||
|
||||
protected $exportData;
|
||||
|
||||
/**
|
||||
* @var ExportManager
|
||||
*/
|
||||
protected $exportManager;
|
||||
|
||||
protected $formatterData;
|
||||
|
||||
/**
|
||||
* This variable cache the labels internally.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $labelsCache;
|
||||
|
||||
protected $result;
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
protected $translator;
|
||||
|
||||
public function __construct(TranslatorInterface $translatorInterface, ExportManager $exportManager)
|
||||
{
|
||||
$this->translator = $translatorInterface;
|
||||
$this->exportManager = $exportManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* build a form, which will be used to collect data required for the execution
|
||||
* of this formatter.
|
||||
*
|
||||
* @uses appendAggregatorForm
|
||||
*
|
||||
* @param type $exportAlias
|
||||
*/
|
||||
public function buildForm(
|
||||
FormBuilderInterface $builder,
|
||||
$exportAlias,
|
||||
array $aggregatorAliases,
|
||||
) {
|
||||
$builder->add('numerotation', ChoiceType::class, [
|
||||
'choices' => [
|
||||
'yes' => true,
|
||||
'no' => false,
|
||||
],
|
||||
'expanded' => true,
|
||||
'multiple' => false,
|
||||
'label' => 'Add a number on first column',
|
||||
]);
|
||||
}
|
||||
|
||||
public function getFormDefaultData(array $aggregatorAliases): array
|
||||
{
|
||||
return ['numerotation' => true];
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return 'CSV vertical list';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a response from the data collected on differents ExportElementInterface.
|
||||
*
|
||||
* @param mixed[] $result The result, as given by the ExportInterface
|
||||
* @param mixed[] $formatterData collected from the current form
|
||||
* @param string $exportAlias the id of the current export
|
||||
* @param array $filtersData an array containing the filters data. The key are the filters id, and the value are the data
|
||||
* @param array $aggregatorsData an array containing the aggregators data. The key are the filters id, and the value are the data
|
||||
*
|
||||
* @return Response The response to be shown
|
||||
*/
|
||||
public function getResponse(
|
||||
$result,
|
||||
$formatterData,
|
||||
$exportAlias,
|
||||
array $exportData,
|
||||
array $filtersData,
|
||||
array $aggregatorsData,
|
||||
) {
|
||||
$this->result = $result;
|
||||
$this->exportAlias = $exportAlias;
|
||||
$this->exportData = $exportData;
|
||||
$this->formatterData = $formatterData;
|
||||
|
||||
$output = fopen('php://output', 'wb');
|
||||
|
||||
$this->prepareHeaders($output);
|
||||
|
||||
$i = 1;
|
||||
|
||||
foreach ($result as $row) {
|
||||
$line = [];
|
||||
|
||||
if (true === $this->formatterData['numerotation']) {
|
||||
$line[] = $i;
|
||||
}
|
||||
|
||||
foreach ($row as $key => $value) {
|
||||
$line[] = $this->getLabel($key, $value);
|
||||
}
|
||||
|
||||
fputcsv($output, $line);
|
||||
|
||||
++$i;
|
||||
}
|
||||
|
||||
$csvContent = stream_get_contents($output);
|
||||
fclose($output);
|
||||
|
||||
$response = new Response();
|
||||
$response->setStatusCode(200);
|
||||
$response->headers->set('Content-Type', 'text/csv; charset=utf-8');
|
||||
// $response->headers->set('Content-Disposition','attachment; filename="export.csv"');
|
||||
|
||||
$response->setContent($csvContent);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function getType()
|
||||
{
|
||||
return FormatterInterface::TYPE_LIST;
|
||||
}
|
||||
|
||||
/**
|
||||
* Give the label corresponding to the given key and value.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws \LogicException if the label is not found
|
||||
*/
|
||||
protected function getLabel($key, $value)
|
||||
{
|
||||
if (null === $this->labelsCache) {
|
||||
$this->prepareCacheLabels();
|
||||
}
|
||||
|
||||
if (!\array_key_exists($key, $this->labelsCache)) {
|
||||
throw new \OutOfBoundsException(sprintf('The key "%s" is not present in the list of keys handled by this query. Check your `getKeys` and `getLabels` methods. Available keys are %s.', $key, \implode(', ', \array_keys($this->labelsCache))));
|
||||
}
|
||||
|
||||
return $this->labelsCache[$key]($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the label cache which will be used by getLabel. This function
|
||||
* should be called only once in the generation lifecycle.
|
||||
*/
|
||||
protected function prepareCacheLabels()
|
||||
{
|
||||
$export = $this->exportManager->getExport($this->exportAlias);
|
||||
$keys = $export->getQueryKeys($this->exportData);
|
||||
|
||||
foreach ($keys as $key) {
|
||||
// get an array with all values for this key if possible
|
||||
$values = \array_map(static fn ($v) => $v[$key], $this->result);
|
||||
// store the label in the labelsCache property
|
||||
$this->labelsCache[$key] = $export->getLabels($key, $values, $this->exportData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* add the headers to the csv file.
|
||||
*
|
||||
* @param resource $output
|
||||
*/
|
||||
protected function prepareHeaders($output)
|
||||
{
|
||||
$keys = $this->exportManager->getExport($this->exportAlias)->getQueryKeys($this->exportData);
|
||||
// we want to keep the order of the first row. So we will iterate on the first row of the results
|
||||
$first_row = \count($this->result) > 0 ? $this->result[0] : [];
|
||||
$header_line = [];
|
||||
|
||||
if (true === $this->formatterData['numerotation']) {
|
||||
$header_line[] = $this->translator->trans('Number');
|
||||
}
|
||||
|
||||
foreach ($first_row as $key => $value) {
|
||||
$header_line[] = $this->translator->trans(
|
||||
$this->getLabel($key, '_header')
|
||||
);
|
||||
}
|
||||
|
||||
if (\count($header_line) > 0) {
|
||||
fputcsv($output, $header_line);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,217 +0,0 @@
|
||||
<?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\Formatter;
|
||||
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
use Chill\MainBundle\Export\FormatterInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* Create a CSV List for the export where the header are printed on the
|
||||
* first column, and the result goes from left to right.
|
||||
*/
|
||||
class CSVPivotedListFormatter implements FormatterInterface
|
||||
{
|
||||
protected $exportAlias;
|
||||
|
||||
protected $exportData;
|
||||
|
||||
/**
|
||||
* @var ExportManager
|
||||
*/
|
||||
protected $exportManager;
|
||||
|
||||
protected $formatterData;
|
||||
|
||||
/**
|
||||
* This variable cache the labels internally.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $labelsCache;
|
||||
|
||||
protected $result;
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
protected $translator;
|
||||
|
||||
public function __construct(TranslatorInterface $translatorInterface, ExportManager $exportManager)
|
||||
{
|
||||
$this->translator = $translatorInterface;
|
||||
$this->exportManager = $exportManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* build a form, which will be used to collect data required for the execution
|
||||
* of this formatter.
|
||||
*
|
||||
* @uses appendAggregatorForm
|
||||
*
|
||||
* @param type $exportAlias
|
||||
*/
|
||||
public function buildForm(
|
||||
FormBuilderInterface $builder,
|
||||
$exportAlias,
|
||||
array $aggregatorAliases,
|
||||
) {
|
||||
$builder->add('numerotation', ChoiceType::class, [
|
||||
'choices' => [
|
||||
'yes' => true,
|
||||
'no' => false,
|
||||
],
|
||||
'expanded' => true,
|
||||
'multiple' => false,
|
||||
'label' => 'Add a number on first column',
|
||||
'data' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getFormDefaultData(array $aggregatorAliases): array
|
||||
{
|
||||
return ['numerotation' => true];
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return 'CSV horizontal list';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a response from the data collected on differents ExportElementInterface.
|
||||
*
|
||||
* @param mixed[] $result The result, as given by the ExportInterface
|
||||
* @param mixed[] $formatterData collected from the current form
|
||||
* @param string $exportAlias the id of the current export
|
||||
* @param array $filtersData an array containing the filters data. The key are the filters id, and the value are the data
|
||||
* @param array $aggregatorsData an array containing the aggregators data. The key are the filters id, and the value are the data
|
||||
*
|
||||
* @return Response The response to be shown
|
||||
*/
|
||||
public function getResponse(
|
||||
$result,
|
||||
$formatterData,
|
||||
$exportAlias,
|
||||
array $exportData,
|
||||
array $filtersData,
|
||||
array $aggregatorsData,
|
||||
) {
|
||||
$this->result = $result;
|
||||
$this->exportAlias = $exportAlias;
|
||||
$this->exportData = $exportData;
|
||||
$this->formatterData = $formatterData;
|
||||
|
||||
$output = fopen('php://output', 'wb');
|
||||
|
||||
$i = 1;
|
||||
$lines = [];
|
||||
$this->prepareHeaders($lines);
|
||||
|
||||
foreach ($result as $row) {
|
||||
$j = 0;
|
||||
|
||||
if (true === $this->formatterData['numerotation']) {
|
||||
$lines[$j][] = $i;
|
||||
++$j;
|
||||
}
|
||||
|
||||
foreach ($row as $key => $value) {
|
||||
$lines[$j][] = $this->getLabel($key, $value);
|
||||
++$j;
|
||||
}
|
||||
++$i;
|
||||
}
|
||||
|
||||
// adding the lines to the csv output
|
||||
foreach ($lines as $line) {
|
||||
fputcsv($output, $line);
|
||||
}
|
||||
|
||||
$csvContent = stream_get_contents($output);
|
||||
fclose($output);
|
||||
|
||||
$response = new Response();
|
||||
$response->setStatusCode(200);
|
||||
$response->headers->set('Content-Type', 'text/csv; charset=utf-8');
|
||||
$response->headers->set('Content-Disposition', 'attachment; filename="export.csv"');
|
||||
|
||||
$response->setContent($csvContent);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function getType()
|
||||
{
|
||||
return FormatterInterface::TYPE_LIST;
|
||||
}
|
||||
|
||||
/**
|
||||
* Give the label corresponding to the given key and value.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws \LogicException if the label is not found
|
||||
*/
|
||||
protected function getLabel($key, $value)
|
||||
{
|
||||
if (null === $this->labelsCache) {
|
||||
$this->prepareCacheLabels();
|
||||
}
|
||||
|
||||
return $this->labelsCache[$key]($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the label cache which will be used by getLabel. This function
|
||||
* should be called only once in the generation lifecycle.
|
||||
*/
|
||||
protected function prepareCacheLabels()
|
||||
{
|
||||
$export = $this->exportManager->getExport($this->exportAlias);
|
||||
$keys = $export->getQueryKeys($this->exportData);
|
||||
|
||||
foreach ($keys as $key) {
|
||||
// get an array with all values for this key if possible
|
||||
$values = \array_map(static fn ($v) => $v[$key], $this->result);
|
||||
// store the label in the labelsCache property
|
||||
$this->labelsCache[$key] = $export->getLabels($key, $values, $this->exportData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* add the headers to lines array.
|
||||
*
|
||||
* @param array $lines the lines where the header will be added
|
||||
*/
|
||||
protected function prepareHeaders(array &$lines)
|
||||
{
|
||||
$keys = $this->exportManager->getExport($this->exportAlias)->getQueryKeys($this->exportData);
|
||||
// we want to keep the order of the first row. So we will iterate on the first row of the results
|
||||
$first_row = \count($this->result) > 0 ? $this->result[0] : [];
|
||||
$header_line = [];
|
||||
|
||||
if (true === $this->formatterData['numerotation']) {
|
||||
$lines[] = [$this->translator->trans('Number')];
|
||||
}
|
||||
|
||||
foreach ($first_row as $key => $value) {
|
||||
$lines[] = [$this->getLabel($key, '_header')];
|
||||
}
|
||||
}
|
||||
}
|
@@ -11,126 +11,32 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Export\Formatter;
|
||||
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
use Chill\MainBundle\Export\ExportGenerationContext;
|
||||
use Chill\MainBundle\Export\ExportInterface;
|
||||
use Chill\MainBundle\Export\ExportManagerAwareInterface;
|
||||
use Chill\MainBundle\Export\FormattedExportGeneration;
|
||||
use Chill\MainBundle\Export\FormatterInterface;
|
||||
use Chill\MainBundle\Export\Helper\ExportManagerAwareTrait;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class SpreadSheetFormatter implements FormatterInterface
|
||||
final class SpreadSheetFormatter implements FormatterInterface, ExportManagerAwareInterface
|
||||
{
|
||||
/**
|
||||
* an array where keys are the aggregators aliases and
|
||||
* values are the data.
|
||||
*
|
||||
* replaced when `getResponse` is called.
|
||||
*
|
||||
* @var type
|
||||
*/
|
||||
protected $aggregatorsData;
|
||||
use ExportManagerAwareTrait;
|
||||
|
||||
/**
|
||||
* The export.
|
||||
*
|
||||
* replaced when `getResponse` is called.
|
||||
*
|
||||
* @var \Chill\MainBundle\Export\ExportInterface
|
||||
*/
|
||||
protected $export;
|
||||
|
||||
/**
|
||||
* replaced when `getResponse` is called.
|
||||
*
|
||||
* @var type
|
||||
*/
|
||||
// protected $aggregators;
|
||||
|
||||
/**
|
||||
* array containing value of export form.
|
||||
*
|
||||
* replaced when `getResponse` is called.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $exportData;
|
||||
|
||||
/**
|
||||
* @var ExportManager
|
||||
*/
|
||||
protected $exportManager;
|
||||
|
||||
/**
|
||||
* replaced when `getResponse` is called.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $filtersData;
|
||||
|
||||
/**
|
||||
* replaced when `getResponse` is called.
|
||||
*
|
||||
* @var type
|
||||
*/
|
||||
protected $formatterData;
|
||||
|
||||
/**
|
||||
* The result, as returned by the export.
|
||||
*
|
||||
* replaced when `getResponse` is called.
|
||||
*
|
||||
* @var type
|
||||
*/
|
||||
protected $result;
|
||||
|
||||
/**
|
||||
* replaced when `getResponse` is called.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
// protected $labels;
|
||||
|
||||
/**
|
||||
* temporary file to store spreadsheet.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $tempfile;
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
protected $translator;
|
||||
|
||||
/**
|
||||
* cache for displayable result.
|
||||
*
|
||||
* This cache is reset when `getResponse` is called.
|
||||
*
|
||||
* The array's keys are the keys in the raw result, and
|
||||
* values are the callable which will transform the raw result to
|
||||
* displayable result.
|
||||
*/
|
||||
private ?array $cacheDisplayableResult = null;
|
||||
|
||||
/**
|
||||
* Whethe `cacheDisplayableResult` is initialized or not.
|
||||
*/
|
||||
private bool $cacheDisplayableResultIsInitialized = false;
|
||||
|
||||
public function __construct(TranslatorInterface $translatorInterface, ExportManager $exportManager)
|
||||
{
|
||||
$this->translator = $translatorInterface;
|
||||
$this->exportManager = $exportManager;
|
||||
}
|
||||
public function __construct(private readonly TranslatorInterface $translator) {}
|
||||
|
||||
public function buildForm(
|
||||
FormBuilderInterface $builder,
|
||||
$exportAlias,
|
||||
array $aggregatorAliases,
|
||||
) {
|
||||
): void {
|
||||
// choosing between formats
|
||||
$builder->add('format', ChoiceType::class, [
|
||||
'choices' => [
|
||||
@@ -142,7 +48,7 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
]);
|
||||
|
||||
// ordering aggregators
|
||||
$aggregators = $this->exportManager->getAggregators($aggregatorAliases);
|
||||
$aggregators = $this->getExportManager()->getAggregators($aggregatorAliases);
|
||||
$nb = \count($aggregatorAliases);
|
||||
|
||||
foreach ($aggregators as $alias => $aggregator) {
|
||||
@@ -155,11 +61,26 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
}
|
||||
}
|
||||
|
||||
public function getNormalizationVersion(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function normalizeFormData(array $formData): array
|
||||
{
|
||||
return $formData;
|
||||
}
|
||||
|
||||
public function denormalizeFormData(array $formData, int $fromVersion): array
|
||||
{
|
||||
return $formData;
|
||||
}
|
||||
|
||||
public function getFormDefaultData(array $aggregatorAliases): array
|
||||
{
|
||||
$data = ['format' => 'xlsx'];
|
||||
|
||||
$aggregators = iterator_to_array($this->exportManager->getAggregators($aggregatorAliases));
|
||||
$aggregators = iterator_to_array($this->getExportManager()->getAggregators($aggregatorAliases));
|
||||
foreach (array_keys($aggregators) as $index => $alias) {
|
||||
$data[$alias] = ['order' => $index + 1];
|
||||
}
|
||||
@@ -172,6 +93,51 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
return 'SpreadSheet (xlsx, ods)';
|
||||
}
|
||||
|
||||
public function generate(
|
||||
$result,
|
||||
$formatterData,
|
||||
string $exportAlias,
|
||||
array $exportData,
|
||||
array $filtersData,
|
||||
array $aggregatorsData,
|
||||
ExportGenerationContext $context,
|
||||
) {
|
||||
// Initialize local variables instead of class properties
|
||||
/** @var ExportInterface $export */
|
||||
$export = $this->getExportManager()->getExport($exportAlias);
|
||||
|
||||
// Initialize cache variables
|
||||
$cacheDisplayableResult = $this->initializeDisplayable($result, $export, $exportData, $aggregatorsData);
|
||||
|
||||
$tempfile = \tempnam(\sys_get_temp_dir(), '');
|
||||
|
||||
if (false === $tempfile) {
|
||||
throw new \RuntimeException('Unable to create temporary file');
|
||||
}
|
||||
|
||||
$this->generateContent(
|
||||
$context,
|
||||
$tempfile,
|
||||
$result,
|
||||
$formatterData,
|
||||
$export,
|
||||
$exportData,
|
||||
$filtersData,
|
||||
$aggregatorsData,
|
||||
$cacheDisplayableResult,
|
||||
);
|
||||
|
||||
$result = new FormattedExportGeneration(
|
||||
file_get_contents($tempfile),
|
||||
$this->getContentType($formatterData['format']),
|
||||
);
|
||||
|
||||
// remove the temp file from disk
|
||||
\unlink($tempfile);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getResponse(
|
||||
$result,
|
||||
$formatterData,
|
||||
@@ -179,44 +145,22 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
array $exportData,
|
||||
array $filtersData,
|
||||
array $aggregatorsData,
|
||||
ExportGenerationContext $context,
|
||||
): Response {
|
||||
// store all data when the process is initiated
|
||||
$this->result = $result;
|
||||
$this->formatterData = $formatterData;
|
||||
$this->export = $this->exportManager->getExport($exportAlias);
|
||||
$this->exportData = $exportData;
|
||||
$this->filtersData = $filtersData;
|
||||
$this->aggregatorsData = $aggregatorsData;
|
||||
$formattedResult = $this->generate($result, $formatterData, $exportAlias, $exportData, $filtersData, $aggregatorsData, $context);
|
||||
|
||||
// reset cache
|
||||
$this->cacheDisplayableResult = [];
|
||||
$this->cacheDisplayableResultIsInitialized = false;
|
||||
|
||||
$response = new Response();
|
||||
$response->headers->set(
|
||||
'Content-Type',
|
||||
$this->getContentType($this->formatterData['format'])
|
||||
);
|
||||
|
||||
$this->tempfile = \tempnam(\sys_get_temp_dir(), '');
|
||||
$this->generateContent();
|
||||
|
||||
$f = \fopen($this->tempfile, 'rb');
|
||||
$response->setContent(\stream_get_contents($f));
|
||||
fclose($f);
|
||||
|
||||
// remove the temp file from disk
|
||||
\unlink($this->tempfile);
|
||||
$response = new BinaryFileResponse($formattedResult->content);
|
||||
$response->headers->set('Content-Type', $formattedResult->contentType);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function getType()
|
||||
public function getType(): string
|
||||
{
|
||||
return 'tabular';
|
||||
}
|
||||
|
||||
protected function addContentTable(
|
||||
private function addContentTable(
|
||||
Worksheet $worksheet,
|
||||
$sortedResults,
|
||||
$line,
|
||||
@@ -238,20 +182,21 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
*
|
||||
* @return int the line number after the last description
|
||||
*/
|
||||
protected function addFiltersDescription(Worksheet &$worksheet)
|
||||
private function addFiltersDescription(Worksheet &$worksheet, ExportGenerationContext $context, array $filtersData)
|
||||
{
|
||||
$line = 3;
|
||||
|
||||
foreach ($this->filtersData as $alias => $data) {
|
||||
$filter = $this->exportManager->getFilter($alias);
|
||||
$description = $filter->describeAction($data, 'string');
|
||||
|
||||
foreach ($filtersData as $alias => $data) {
|
||||
$filter = $this->getExportManager()->getFilter($alias);
|
||||
$description = $filter->describeAction($data, $context);
|
||||
if (\is_array($description)) {
|
||||
$description = $this->translator
|
||||
->trans(
|
||||
$description[0],
|
||||
$description[1] ?? []
|
||||
$description[1] ?? [],
|
||||
);
|
||||
} elseif ($description instanceof TranslatableInterface) {
|
||||
$description = $description->trans($this->translator, $this->translator->getLocale());
|
||||
}
|
||||
|
||||
$worksheet->setCellValue('A'.$line, $description);
|
||||
@@ -266,23 +211,23 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
*
|
||||
* return the line number where the next content (i.e. result) should
|
||||
* be appended.
|
||||
*
|
||||
* @param int $line
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function addHeaders(
|
||||
private function addHeaders(
|
||||
Worksheet &$worksheet,
|
||||
array $globalKeys,
|
||||
$line,
|
||||
) {
|
||||
int $line,
|
||||
array $cacheDisplayableResult = [],
|
||||
): int {
|
||||
// get the displayable form of headers
|
||||
$displayables = [];
|
||||
|
||||
foreach ($globalKeys as $key) {
|
||||
$displayables[] = $this->translator->trans(
|
||||
$this->getDisplayableResult($key, '_header')
|
||||
);
|
||||
$displayable = $this->getDisplayableResult($key, '_header', $cacheDisplayableResult);
|
||||
|
||||
if ($displayable instanceof TranslatableInterface) {
|
||||
$displayables[] = $displayable->trans($this->translator, $this->translator->getLocale());
|
||||
} else {
|
||||
$displayables[] = $this->translator->trans($this->getDisplayableResult($key, '_header', $cacheDisplayableResult));
|
||||
}
|
||||
}
|
||||
|
||||
// add headers on worksheet
|
||||
@@ -299,9 +244,9 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
* Add the title to the worksheet and merge the cell containing
|
||||
* the title.
|
||||
*/
|
||||
protected function addTitleToWorkSheet(Worksheet &$worksheet)
|
||||
private function addTitleToWorkSheet(Worksheet &$worksheet, $export)
|
||||
{
|
||||
$worksheet->setCellValue('A1', $this->getTitle());
|
||||
$worksheet->setCellValue('A1', $this->getTitle($export));
|
||||
$worksheet->mergeCells('A1:G1');
|
||||
}
|
||||
|
||||
@@ -310,14 +255,14 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
*
|
||||
* @return array where 1st member is spreadsheet, 2nd is worksheet
|
||||
*/
|
||||
protected function createSpreadsheet()
|
||||
private function createSpreadsheet($export)
|
||||
{
|
||||
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
// setting the worksheet title and code name
|
||||
$worksheet
|
||||
->setTitle($this->getTitle())
|
||||
->setTitle($this->getTitle($export))
|
||||
->setCodeName('result');
|
||||
|
||||
return [$spreadsheet, $worksheet];
|
||||
@@ -326,29 +271,38 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
/**
|
||||
* Generate the content and write it to php://temp.
|
||||
*/
|
||||
protected function generateContent()
|
||||
{
|
||||
[$spreadsheet, $worksheet] = $this->createSpreadsheet();
|
||||
private function generateContent(
|
||||
ExportGenerationContext $context,
|
||||
string $tempfile,
|
||||
$result,
|
||||
$formatterData,
|
||||
$export,
|
||||
array $exportData,
|
||||
array $filtersData,
|
||||
array $aggregatorsData,
|
||||
array $cacheDisplayableResult,
|
||||
) {
|
||||
[$spreadsheet, $worksheet] = $this->createSpreadsheet($export);
|
||||
|
||||
$this->addTitleToWorkSheet($worksheet);
|
||||
$line = $this->addFiltersDescription($worksheet);
|
||||
$this->addTitleToWorkSheet($worksheet, $export);
|
||||
$line = $this->addFiltersDescription($worksheet, $context, $filtersData);
|
||||
|
||||
// at this point, we are going to sort retsults for an easier manipulation
|
||||
// at this point, we are going to sort results for an easier manipulation
|
||||
[$sortedResult, $exportKeys, $aggregatorKeys, $globalKeys] =
|
||||
$this->sortResult();
|
||||
$this->sortResult($result, $export, $exportData, $aggregatorsData, $formatterData, $cacheDisplayableResult);
|
||||
|
||||
$line = $this->addHeaders($worksheet, $globalKeys, $line);
|
||||
$line = $this->addHeaders($worksheet, $globalKeys, $line, $cacheDisplayableResult);
|
||||
|
||||
$line = $this->addContentTable($worksheet, $sortedResult, $line);
|
||||
$this->addContentTable($worksheet, $sortedResult, $line);
|
||||
|
||||
$writer = match ($this->formatterData['format']) {
|
||||
$writer = match ($formatterData['format']) {
|
||||
'ods' => \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Ods'),
|
||||
'xlsx' => \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xlsx'),
|
||||
'csv' => \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Csv'),
|
||||
default => throw new \LogicException(),
|
||||
};
|
||||
|
||||
$writer->save($this->tempfile);
|
||||
$writer->save($tempfile);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -357,7 +311,7 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
*
|
||||
* @return string[] an array containing the keys of aggregators
|
||||
*/
|
||||
protected function getAggregatorKeysSorted()
|
||||
private function getAggregatorKeysSorted(array $aggregatorsData, array $formatterData)
|
||||
{
|
||||
// empty array for aggregators keys
|
||||
$keys = [];
|
||||
@@ -365,7 +319,7 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
// during sorting
|
||||
$aggregatorKeyAssociation = [];
|
||||
|
||||
foreach ($this->aggregatorsData as $alias => $data) {
|
||||
foreach ($aggregatorsData as $alias => $data) {
|
||||
$aggregator = $this->exportManager->getAggregator($alias);
|
||||
$aggregatorsKeys = $aggregator->getQueryKeys($data);
|
||||
// append the keys from aggregator to the $keys existing array
|
||||
@@ -377,9 +331,9 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
}
|
||||
|
||||
// sort the result using the form
|
||||
usort($keys, function ($a, $b) use ($aggregatorKeyAssociation) {
|
||||
$A = $this->formatterData[$aggregatorKeyAssociation[$a]]['order'];
|
||||
$B = $this->formatterData[$aggregatorKeyAssociation[$b]]['order'];
|
||||
usort($keys, function ($a, $b) use ($aggregatorKeyAssociation, $formatterData) {
|
||||
$A = $formatterData[$aggregatorKeyAssociation[$a]]['order'];
|
||||
$B = $formatterData[$aggregatorKeyAssociation[$b]]['order'];
|
||||
|
||||
if ($A === $B) {
|
||||
return 0;
|
||||
@@ -395,7 +349,7 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
return $keys;
|
||||
}
|
||||
|
||||
protected function getContentType($format)
|
||||
private function getContentType($format)
|
||||
{
|
||||
switch ($format) {
|
||||
case 'csv':
|
||||
@@ -412,25 +366,26 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
|
||||
/**
|
||||
* Get the displayable result.
|
||||
*
|
||||
* @param string $key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getDisplayableResult($key, mixed $value)
|
||||
{
|
||||
if (false === $this->cacheDisplayableResultIsInitialized) {
|
||||
$this->initializeCache($key);
|
||||
}
|
||||
|
||||
private function getDisplayableResult(
|
||||
string $key,
|
||||
mixed $value,
|
||||
array $cacheDisplayableResult,
|
||||
): string|TranslatableInterface|\DateTimeInterface|int|float|bool {
|
||||
$value ??= '';
|
||||
|
||||
return \call_user_func($this->cacheDisplayableResult[$key], $value);
|
||||
return \call_user_func($cacheDisplayableResult[$key], $value);
|
||||
}
|
||||
|
||||
protected function getTitle()
|
||||
private function getTitle($export): string
|
||||
{
|
||||
$title = $this->translator->trans($this->export->getTitle());
|
||||
$original = $export->getTitle();
|
||||
|
||||
if ($original instanceof TranslatableInterface) {
|
||||
$title = $original->trans($this->translator, $this->translator->getLocale());
|
||||
} else {
|
||||
$title = $this->translator->trans($original);
|
||||
}
|
||||
|
||||
if (30 < strlen($title)) {
|
||||
return substr($title, 0, 30).'…';
|
||||
@@ -439,8 +394,13 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
return $title;
|
||||
}
|
||||
|
||||
protected function initializeCache($key)
|
||||
{
|
||||
private function initializeDisplayable(
|
||||
$result,
|
||||
ExportInterface $export,
|
||||
array $exportData,
|
||||
array $aggregatorsData,
|
||||
): array {
|
||||
$cacheDisplayableResult = [];
|
||||
/*
|
||||
* this function follows the following steps :
|
||||
*
|
||||
@@ -453,13 +413,12 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
// 1. create an associative array with key and export / aggregator
|
||||
$keysExportElementAssociation = [];
|
||||
// keys for export
|
||||
foreach ($this->export->getQueryKeys($this->exportData) as $key) {
|
||||
$keysExportElementAssociation[$key] = [$this->export,
|
||||
$this->exportData, ];
|
||||
foreach ($export->getQueryKeys($exportData) as $key) {
|
||||
$keysExportElementAssociation[$key] = [$export, $exportData];
|
||||
}
|
||||
// keys for aggregator
|
||||
foreach ($this->aggregatorsData as $alias => $data) {
|
||||
$aggregator = $this->exportManager->getAggregator($alias);
|
||||
foreach ($aggregatorsData as $alias => $data) {
|
||||
$aggregator = $this->getExportManager()->getAggregator($alias);
|
||||
|
||||
foreach ($aggregator->getQueryKeys($data) as $key) {
|
||||
$keysExportElementAssociation[$key] = [$aggregator, $data];
|
||||
@@ -471,7 +430,7 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
|
||||
$allValues = [];
|
||||
// store all the values in an array
|
||||
foreach ($this->result as $row) {
|
||||
foreach ($result as $row) {
|
||||
foreach ($keys as $key) {
|
||||
$allValues[$key][] = $row[$key];
|
||||
}
|
||||
@@ -482,15 +441,14 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
foreach ($keysExportElementAssociation as $key => [$element, $data]) {
|
||||
// handle the case when there is not results lines (query is empty)
|
||||
if ([] === $allValues) {
|
||||
$this->cacheDisplayableResult[$key] = $element->getLabels($key, ['_header'], $data);
|
||||
$cacheDisplayableResult[$key] = $element->getLabels($key, ['_header'], $data);
|
||||
} else {
|
||||
$this->cacheDisplayableResult[$key] =
|
||||
$cacheDisplayableResult[$key] =
|
||||
$element->getLabels($key, \array_unique($allValues[$key]), $data);
|
||||
}
|
||||
}
|
||||
|
||||
// the cache is initialized !
|
||||
$this->cacheDisplayableResultIsInitialized = true;
|
||||
return $cacheDisplayableResult;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -528,23 +486,28 @@ class SpreadSheetFormatter implements FormatterInterface
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
protected function sortResult()
|
||||
{
|
||||
private function sortResult(
|
||||
$result,
|
||||
ExportInterface $export,
|
||||
array $exportData,
|
||||
array $aggregatorsData,
|
||||
array $formatterData,
|
||||
array $cacheDisplayableResult,
|
||||
) {
|
||||
// get the keys for each row
|
||||
$exportKeys = $this->export->getQueryKeys($this->exportData);
|
||||
$aggregatorKeys = $this->getAggregatorKeysSorted();
|
||||
|
||||
$exportKeys = $export->getQueryKeys($exportData);
|
||||
$aggregatorKeys = $this->getAggregatorKeysSorted($aggregatorsData, $formatterData);
|
||||
$globalKeys = \array_merge($aggregatorKeys, $exportKeys);
|
||||
|
||||
$sortedResult = \array_map(function ($row) use ($globalKeys) {
|
||||
$sortedResult = \array_map(function ($row) use ($globalKeys, $cacheDisplayableResult) {
|
||||
$newRow = [];
|
||||
|
||||
foreach ($globalKeys as $key) {
|
||||
$newRow[] = $this->getDisplayableResult($key, $row[$key]);
|
||||
$newRow[] = $this->getDisplayableResult($key, $row[$key], $cacheDisplayableResult);
|
||||
}
|
||||
|
||||
return $newRow;
|
||||
}, $this->result);
|
||||
}, $result);
|
||||
|
||||
\array_multisort($sortedResult);
|
||||
|
||||
|
@@ -11,68 +11,42 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Export\Formatter;
|
||||
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
use Chill\MainBundle\Export\ExportGenerationContext;
|
||||
use Chill\MainBundle\Export\ExportManagerAwareInterface;
|
||||
use Chill\MainBundle\Export\FormattedExportGeneration;
|
||||
use Chill\MainBundle\Export\FormatterInterface;
|
||||
use Chill\MainBundle\Export\Helper\ExportManagerAwareTrait;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
// command to get the report with curl : curl --user "center a_social:password" "http://localhost:8000/fr/exports/generate/count_person?export[filters][person_gender_filter][enabled]=&export[filters][person_nationality_filter][enabled]=&export[filters][person_nationality_filter][form][nationalities]=&export[aggregators][person_nationality_aggregator][order]=1&export[aggregators][person_nationality_aggregator][form][group_by_level]=country&export[submit]=&export[_token]=RHpjHl389GrK-bd6iY5NsEqrD5UKOTHH40QKE9J1edU" --globoff
|
||||
|
||||
/**
|
||||
* Create a CSV List for the export.
|
||||
*/
|
||||
class SpreadsheetListFormatter implements FormatterInterface
|
||||
class SpreadsheetListFormatter implements FormatterInterface, ExportManagerAwareInterface
|
||||
{
|
||||
protected $exportAlias;
|
||||
use ExportManagerAwareTrait;
|
||||
|
||||
protected $exportData;
|
||||
|
||||
/**
|
||||
* @var ExportManager
|
||||
*/
|
||||
protected $exportManager;
|
||||
|
||||
protected $formatterData;
|
||||
|
||||
/**
|
||||
* This variable cache the labels internally.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $labelsCache;
|
||||
|
||||
protected $result;
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
protected $translator;
|
||||
|
||||
public function __construct(TranslatorInterface $translatorInterface, ExportManager $exportManager)
|
||||
{
|
||||
$this->translator = $translatorInterface;
|
||||
$this->exportManager = $exportManager;
|
||||
}
|
||||
public function __construct(private readonly TranslatorInterface $translator) {}
|
||||
|
||||
/**
|
||||
* build a form, which will be used to collect data required for the execution
|
||||
* of this formatter.
|
||||
*
|
||||
* @uses appendAggregatorForm
|
||||
*
|
||||
* @param string $exportAlias
|
||||
*/
|
||||
public function buildForm(
|
||||
FormBuilderInterface $builder,
|
||||
$exportAlias,
|
||||
string $exportAlias,
|
||||
array $aggregatorAliases,
|
||||
) {
|
||||
): void {
|
||||
$builder
|
||||
->add('format', ChoiceType::class, [
|
||||
'choices' => [
|
||||
@@ -92,58 +66,52 @@ class SpreadsheetListFormatter implements FormatterInterface
|
||||
]);
|
||||
}
|
||||
|
||||
public function getNormalizationVersion(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function normalizeFormData(array $formData): array
|
||||
{
|
||||
return ['format' => $formData['format'], 'numerotation' => $formData['numerotation']];
|
||||
}
|
||||
|
||||
public function denormalizeFormData(array $formData, int $fromVersion): array
|
||||
{
|
||||
return ['format' => $formData['format'], 'numerotation' => $formData['numerotation']];
|
||||
}
|
||||
|
||||
public function getFormDefaultData(array $aggregatorAliases): array
|
||||
{
|
||||
return ['numerotation' => true, 'format' => 'xlsx'];
|
||||
}
|
||||
|
||||
public function getName()
|
||||
public function getName(): string|TranslatableInterface
|
||||
{
|
||||
return 'Spreadsheet list formatter (.xlsx, .ods)';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a response from the data collected on differents ExportElementInterface.
|
||||
*
|
||||
* @param mixed[] $result The result, as given by the ExportInterface
|
||||
* @param mixed[] $formatterData collected from the current form
|
||||
* @param string $exportAlias the id of the current export
|
||||
* @param array $filtersData an array containing the filters data. The key are the filters id, and the value are the data
|
||||
* @param array $aggregatorsData an array containing the aggregators data. The key are the filters id, and the value are the data
|
||||
*
|
||||
* @return Response The response to be shown
|
||||
*/
|
||||
public function getResponse(
|
||||
$result,
|
||||
$formatterData,
|
||||
$exportAlias,
|
||||
array $exportData,
|
||||
array $filtersData,
|
||||
array $aggregatorsData,
|
||||
) {
|
||||
$this->result = $result;
|
||||
$this->exportAlias = $exportAlias;
|
||||
$this->exportData = $exportData;
|
||||
$this->formatterData = $formatterData;
|
||||
|
||||
public function generate($result, $formatterData, string $exportAlias, array $exportData, array $filtersData, array $aggregatorsData, ExportGenerationContext $context): FormattedExportGeneration
|
||||
{
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
$cacheLabels = $this->prepareCacheLabels($result, $exportAlias, $exportData);
|
||||
|
||||
$this->prepareHeaders($worksheet);
|
||||
$this->prepareHeaders($cacheLabels, $worksheet, $result, $formatterData, $exportAlias, $exportData);
|
||||
|
||||
$i = 1;
|
||||
|
||||
foreach ($result as $row) {
|
||||
if (true === $this->formatterData['numerotation']) {
|
||||
if (true === $formatterData['numerotation']) {
|
||||
$worksheet->setCellValue('A'.($i + 1), (string) $i);
|
||||
}
|
||||
|
||||
$a = $this->formatterData['numerotation'] ? 'B' : 'A';
|
||||
$a = $formatterData['numerotation'] ? 'B' : 'A';
|
||||
|
||||
foreach ($row as $key => $value) {
|
||||
$row = $a.($i + 1);
|
||||
|
||||
$formattedValue = $this->getLabel($key, $value);
|
||||
$formattedValue = $this->getLabel($cacheLabels, $key, $value, $result, $exportAlias, $exportData);
|
||||
|
||||
if ($formattedValue instanceof \DateTimeInterface) {
|
||||
$worksheet->setCellValue($row, Date::PHPToExcel($formattedValue));
|
||||
@@ -157,6 +125,8 @@ class SpreadsheetListFormatter implements FormatterInterface
|
||||
->getNumberFormat()
|
||||
->setFormatCode(NumberFormat::FORMAT_DATE_DATETIME);
|
||||
}
|
||||
} elseif ($formattedValue instanceof TranslatableInterface) {
|
||||
$worksheet->setCellValue($row, $formattedValue->trans($this->translator));
|
||||
} else {
|
||||
$worksheet->setCellValue($row, $formattedValue);
|
||||
}
|
||||
@@ -166,7 +136,7 @@ class SpreadsheetListFormatter implements FormatterInterface
|
||||
++$i;
|
||||
}
|
||||
|
||||
switch ($this->formatterData['format']) {
|
||||
switch ($formatterData['format']) {
|
||||
case 'ods':
|
||||
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Ods');
|
||||
$contentType = 'application/vnd.oasis.opendocument.spreadsheet';
|
||||
@@ -189,26 +159,52 @@ class SpreadsheetListFormatter implements FormatterInterface
|
||||
default:
|
||||
// this should not happen
|
||||
// throw an exception to ensure that the error is catched
|
||||
throw new \OutOfBoundsException('The format '.$this->formatterData['format'].' is not supported');
|
||||
throw new \OutOfBoundsException('The format '.$formatterData['format'].' is not supported');
|
||||
}
|
||||
|
||||
$response = new Response();
|
||||
$response->headers->set('content-type', $contentType);
|
||||
|
||||
$tempfile = \tempnam(\sys_get_temp_dir(), '');
|
||||
$writer->save($tempfile);
|
||||
|
||||
$f = \fopen($tempfile, 'rb');
|
||||
$response->setContent(\stream_get_contents($f));
|
||||
fclose($f);
|
||||
$generated = new FormattedExportGeneration(
|
||||
file_get_contents($tempfile),
|
||||
$contentType,
|
||||
);
|
||||
|
||||
// remove the temp file from disk
|
||||
\unlink($tempfile);
|
||||
|
||||
return $generated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a response from the data collected on differents ExportElementInterface.
|
||||
*
|
||||
* @param mixed[] $result The result, as given by the ExportInterface
|
||||
* @param mixed[] $formatterData collected from the current form
|
||||
* @param string $exportAlias the id of the current export
|
||||
* @param array $aggregatorsData an array containing the aggregators data. The key are the filters id, and the value are the data
|
||||
* @param array $filtersData an array containing the filters data. The key are the filters id, and the value are the data
|
||||
*
|
||||
* @return Response The response to be shown
|
||||
*/
|
||||
public function getResponse(
|
||||
$result,
|
||||
$formatterData,
|
||||
$exportAlias,
|
||||
array $exportData,
|
||||
array $filtersData,
|
||||
array $aggregatorsData,
|
||||
ExportGenerationContext $context,
|
||||
) {
|
||||
$generated = $this->generate($result, $formatterData, $exportAlias, $exportData, $filtersData, $aggregatorsData, $context);
|
||||
|
||||
$response = new BinaryFileResponse($generated->content);
|
||||
$response->headers->set('Content-Type', $generated->contentType);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function getType()
|
||||
public function getType(): string
|
||||
{
|
||||
return FormatterInterface::TYPE_LIST;
|
||||
}
|
||||
@@ -216,34 +212,29 @@ class SpreadsheetListFormatter implements FormatterInterface
|
||||
/**
|
||||
* Give the label corresponding to the given key and value.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
*
|
||||
* @return string
|
||||
* @return string|\DateTimeInterface|int|float|TranslatableInterface|null
|
||||
*
|
||||
* @throws \LogicException if the label is not found
|
||||
*/
|
||||
protected function getLabel($key, $value)
|
||||
private function getLabel(array $labelsCache, $key, $value, array $result, string $exportAlias, array $exportData)
|
||||
{
|
||||
if (null === $this->labelsCache) {
|
||||
$this->prepareCacheLabels();
|
||||
if (!\array_key_exists($key, $labelsCache)) {
|
||||
throw new \OutOfBoundsException(sprintf('The key "%s" is not present in the list of keys handled by this query. Check your `getKeys` and `getLabels` methods. Available keys are %s.', $key, \implode(', ', \array_keys($labelsCache))));
|
||||
}
|
||||
|
||||
if (!\array_key_exists($key, $this->labelsCache)) {
|
||||
throw new \OutOfBoundsException(sprintf('The key "%s" is not present in the list of keys handled by this query. Check your `getKeys` and `getLabels` methods. Available keys are %s.', $key, \implode(', ', \array_keys($this->labelsCache))));
|
||||
}
|
||||
|
||||
return $this->labelsCache[$key]($value);
|
||||
return $labelsCache[$key]($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the label cache which will be used by getLabel. This function
|
||||
* should be called only once in the generation lifecycle.
|
||||
* Prepare the label cache which will be used by getLabel.
|
||||
*
|
||||
* @return array The labels cache
|
||||
*/
|
||||
protected function prepareCacheLabels()
|
||||
private function prepareCacheLabels(array $result, string $exportAlias, array $exportData): array
|
||||
{
|
||||
$export = $this->exportManager->getExport($this->exportAlias);
|
||||
$keys = $export->getQueryKeys($this->exportData);
|
||||
$labelsCache = [];
|
||||
$export = $this->getExportManager()->getExport($exportAlias);
|
||||
$keys = $export->getQueryKeys($exportData);
|
||||
|
||||
foreach ($keys as $key) {
|
||||
// get an array with all values for this key if possible
|
||||
@@ -253,29 +244,31 @@ class SpreadsheetListFormatter implements FormatterInterface
|
||||
}
|
||||
|
||||
return $v[$key];
|
||||
}, $this->result);
|
||||
// store the label in the labelsCache property
|
||||
$this->labelsCache[$key] = $export->getLabels($key, $values, $this->exportData);
|
||||
}, $result);
|
||||
// store the label in the labelsCache
|
||||
$labelsCache[$key] = $export->getLabels($key, $values, $exportData);
|
||||
}
|
||||
|
||||
return $labelsCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* add the headers to the csv file.
|
||||
*/
|
||||
protected function prepareHeaders(Worksheet $worksheet)
|
||||
protected function prepareHeaders(array $labelsCache, Worksheet $worksheet, array $result, array $formatterData, string $exportAlias, array $exportData)
|
||||
{
|
||||
$keys = $this->exportManager->getExport($this->exportAlias)->getQueryKeys($this->exportData);
|
||||
$keys = $this->getExportManager()->getExport($exportAlias)->getQueryKeys($exportData);
|
||||
// we want to keep the order of the first row. So we will iterate on the first row of the results
|
||||
$first_row = \count($this->result) > 0 ? $this->result[0] : [];
|
||||
$first_row = \count($result) > 0 ? $result[0] : [];
|
||||
$header_line = [];
|
||||
|
||||
if (true === $this->formatterData['numerotation']) {
|
||||
if (true === $formatterData['numerotation']) {
|
||||
$header_line[] = $this->translator->trans('Number');
|
||||
}
|
||||
|
||||
foreach ($first_row as $key => $value) {
|
||||
$header_line[] = $this->translator->trans(
|
||||
$this->getLabel($key, '_header')
|
||||
$this->getLabel($labelsCache, $key, '_header', $result, $exportAlias, $exportData)
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -12,7 +12,11 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Export;
|
||||
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
|
||||
/**
|
||||
* @method generate($result, $formatterData, string $exportAlias, array $exportData, array $filtersData, array $aggregatorsData, ExportGenerationContext $context): FormattedExportGeneration
|
||||
*/
|
||||
interface FormatterInterface
|
||||
{
|
||||
public const TYPE_LIST = 'list';
|
||||
@@ -30,16 +34,16 @@ interface FormatterInterface
|
||||
*/
|
||||
public function buildForm(
|
||||
FormBuilderInterface $builder,
|
||||
$exportAlias,
|
||||
string $exportAlias,
|
||||
array $aggregatorAliases,
|
||||
);
|
||||
): void;
|
||||
|
||||
/**
|
||||
* get the default data for the form build by buildForm.
|
||||
*/
|
||||
public function getFormDefaultData(array $aggregatorAliases): array;
|
||||
|
||||
public function getName();
|
||||
public function getName(): string|TranslatableInterface;
|
||||
|
||||
/**
|
||||
* Generate a response from the data collected on differents ExportElementInterface.
|
||||
@@ -47,19 +51,28 @@ interface FormatterInterface
|
||||
* @param mixed[] $result The result, as given by the ExportInterface
|
||||
* @param mixed[] $formatterData collected from the current form
|
||||
* @param string $exportAlias the id of the current export
|
||||
* @param array $filtersData an array containing the filters data. The key are the filters id, and the value are the data
|
||||
* @param array $aggregatorsData an array containing the aggregators data. The key are the filters id, and the value are the data
|
||||
* @param array $filtersData an array containing the filters data. The key are the filters id, and the value are the data
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response The response to be shown
|
||||
*
|
||||
* @deprecated use generate instead
|
||||
*/
|
||||
public function getResponse(
|
||||
$result,
|
||||
$formatterData,
|
||||
$exportAlias,
|
||||
array $result,
|
||||
array $formatterData,
|
||||
string $exportAlias,
|
||||
array $exportData,
|
||||
array $filtersData,
|
||||
array $aggregatorsData,
|
||||
ExportGenerationContext $context,
|
||||
);
|
||||
|
||||
public function getType();
|
||||
public function getType(): string;
|
||||
|
||||
public function normalizeFormData(array $formData): array;
|
||||
|
||||
public function denormalizeFormData(array $formData, int $fromVersion): array;
|
||||
|
||||
public function getNormalizationVersion(): int;
|
||||
}
|
||||
|
@@ -0,0 +1,34 @@
|
||||
<?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\Helper;
|
||||
|
||||
use Chill\MainBundle\Export\Exception\ExportRuntimeException;
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
|
||||
trait ExportManagerAwareTrait
|
||||
{
|
||||
private ?ExportManager $exportManager;
|
||||
|
||||
public function setExportManager(ExportManager $exportManager): void
|
||||
{
|
||||
$this->exportManager = $exportManager;
|
||||
}
|
||||
|
||||
public function getExportManager(): ExportManager
|
||||
{
|
||||
if (null === $this->exportManager) {
|
||||
throw new ExportRuntimeException('ExportManager not set');
|
||||
}
|
||||
|
||||
return $this->exportManager;
|
||||
}
|
||||
}
|
@@ -11,6 +11,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Export;
|
||||
|
||||
use Doctrine\ORM\NativeQuery;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
/**
|
||||
* Define methods to export list.
|
||||
*
|
||||
@@ -19,5 +22,10 @@ namespace Chill\MainBundle\Export;
|
||||
* (and list does not support aggregation on their data).
|
||||
*
|
||||
* When used, the `ExportManager` will not handle aggregator for this class.
|
||||
*
|
||||
* @template Q of QueryBuilder|NativeQuery
|
||||
* @template D of array
|
||||
*
|
||||
* @template-extends ExportInterface<Q, D>
|
||||
*/
|
||||
interface ListInterface extends ExportInterface {}
|
||||
|
@@ -0,0 +1,31 @@
|
||||
<?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;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
final readonly class ExportRequestGenerationMessage
|
||||
{
|
||||
public UuidInterface $id;
|
||||
|
||||
public int $userId;
|
||||
|
||||
public function __construct(
|
||||
ExportGeneration $exportGeneration,
|
||||
User $user,
|
||||
) {
|
||||
$this->id = $exportGeneration->getId();
|
||||
$this->userId = $user->getId();
|
||||
}
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
<?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\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Chill\MainBundle\Export\ExportGenerator;
|
||||
use Chill\MainBundle\Repository\ExportGenerationRepository;
|
||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
|
||||
#[AsMessageHandler]
|
||||
final readonly class ExportRequestGenerationMessageHandler implements MessageHandlerInterface
|
||||
{
|
||||
private const LOG_PREFIX = '[export_generation] ';
|
||||
|
||||
public function __construct(
|
||||
private ExportGenerationRepository $repository,
|
||||
private UserRepositoryInterface $userRepository,
|
||||
private ExportGenerator $exportGenerator,
|
||||
private StoredObjectManagerInterface $storedObjectManager,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
public function __invoke(ExportRequestGenerationMessage $exportRequestGenerationMessage)
|
||||
{
|
||||
$start = microtime(true);
|
||||
|
||||
$this->logger->info(
|
||||
self::LOG_PREFIX.'Handle generation message',
|
||||
[
|
||||
'exportId' => (string) $exportRequestGenerationMessage->id,
|
||||
]
|
||||
);
|
||||
|
||||
if (null === $exportGeneration = $this->repository->find($exportRequestGenerationMessage->id)) {
|
||||
throw new \UnexpectedValueException('ExportRequestGenerationMessage not found');
|
||||
}
|
||||
|
||||
if (null === $user = $this->userRepository->find($exportRequestGenerationMessage->userId)) {
|
||||
throw new \UnexpectedValueException('User not found');
|
||||
}
|
||||
|
||||
if (StoredObject::STATUS_PENDING !== $exportGeneration->getStatus()) {
|
||||
throw new UnrecoverableMessageHandlingException('object already generated');
|
||||
}
|
||||
|
||||
$generated = $this->exportGenerator->generate($exportGeneration->getExportAlias(), $exportGeneration->getOptions(), $user);
|
||||
$this->storedObjectManager->write($exportGeneration->getStoredObject(), $generated->content, $generated->contentType);
|
||||
$exportGeneration->getStoredObject()->setStatus(StoredObject::STATUS_READY);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$end = microtime(true);
|
||||
|
||||
$this->logger->notice(self::LOG_PREFIX.'Export generation successfully finished', [
|
||||
'exportId' => (string) $exportRequestGenerationMessage->id,
|
||||
'exportAlias' => $exportGeneration->getExportAlias(),
|
||||
'full_generation_duration' => $end - $exportGeneration->getCreatedAt()->getTimestamp(),
|
||||
'message_handler_duration' => $end - $start,
|
||||
]);
|
||||
}
|
||||
}
|
@@ -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\Messenger;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\MainBundle\Entity\ExportGeneration;
|
||||
use Chill\MainBundle\Repository\ExportGenerationRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
||||
|
||||
class OnExportGenerationFails implements EventSubscriberInterface
|
||||
{
|
||||
private const LOG_PREFIX = '[export_generation failed] ';
|
||||
|
||||
public function __construct(
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly ExportGenerationRepository $repository,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
WorkerMessageFailedEvent::class => 'onMessageFailed',
|
||||
];
|
||||
}
|
||||
|
||||
public function onMessageFailed(WorkerMessageFailedEvent $event): void
|
||||
{
|
||||
if ($event->willRetry()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = $event->getEnvelope()->getMessage();
|
||||
|
||||
if (!$message instanceof ExportRequestGenerationMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null === $exportGeneration = $this->repository->find($message->id)) {
|
||||
throw new \UnexpectedValueException('ExportRequestGenerationMessage not found');
|
||||
}
|
||||
|
||||
$this->logger->error(self::LOG_PREFIX.'ExportRequestGenerationMessage failed to execute generation', [
|
||||
'exportId' => (string) $message->id,
|
||||
'userId' => $message->userId,
|
||||
'alias' => $exportGeneration->getExportAlias(),
|
||||
'throwable_message' => $event->getThrowable()->getMessage(),
|
||||
'throwable_trace' => $event->getThrowable()->getTraceAsString(),
|
||||
'throwable' => $event->getThrowable()::class,
|
||||
'full_generation_duration_failure' => microtime(true) - $exportGeneration->getCreatedAt()->getTimestamp(),
|
||||
]);
|
||||
|
||||
$this->markObjectAsFailed($event, $exportGeneration);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
private function markObjectAsFailed(WorkerMessageFailedEvent $event, ExportGeneration $exportGeneration): void
|
||||
{
|
||||
$exportGeneration->getStoredObject()->addGenerationErrors($event->getThrowable()->getMessage());
|
||||
$exportGeneration->getStoredObject()->setStatus(StoredObject::STATUS_FAILURE);
|
||||
}
|
||||
}
|
@@ -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]
|
||||
final readonly 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();
|
||||
}
|
||||
}
|
@@ -0,0 +1,124 @@
|
||||
<?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\Migrator;
|
||||
|
||||
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||
|
||||
class SavedExportOptionsMigrator
|
||||
{
|
||||
public static function migrate(array $fromOptions): array
|
||||
{
|
||||
$to = [];
|
||||
$to['aggregators'] = array_map(
|
||||
self::mapEnabledStatus(...),
|
||||
$fromOptions['export']['export']['aggregators'] ?? [],
|
||||
);
|
||||
|
||||
$to['filters'] = array_map(
|
||||
self::mapEnabledStatus(...),
|
||||
$fromOptions['export']['export']['filters'] ?? [],
|
||||
);
|
||||
|
||||
$to['export'] = [
|
||||
'form' => array_map(
|
||||
self::mapFormData(...),
|
||||
array_filter(
|
||||
$fromOptions['export']['export']['export'] ?? [],
|
||||
static fn (string $key) => !in_array($key, ['filters', 'aggregators', 'pick_formatter'], true),
|
||||
ARRAY_FILTER_USE_KEY,
|
||||
),
|
||||
),
|
||||
'version' => 1,
|
||||
];
|
||||
|
||||
$to['pick_formatter'] = $fromOptions['export']['export']['pick_formatter']['alias'] ?? null;
|
||||
$to['centers'] = [
|
||||
'centers' => array_values(array_map(static fn ($id) => (int) $id, $fromOptions['centers']['centers']['c'] ?? $fromOptions['centers']['centers']['center'] ?? [])),
|
||||
'regroupments' => array_values(array_map(static fn ($id) => (int) $id, $fromOptions['centers']['centers']['regroupment'] ?? [])),
|
||||
];
|
||||
$to['formatter'] = [
|
||||
'form' => $fromOptions['formatter']['formatter'] ?? [],
|
||||
'version' => 1,
|
||||
];
|
||||
|
||||
return $to;
|
||||
}
|
||||
|
||||
private static function mapEnabledStatus(array $modifiersData): array
|
||||
{
|
||||
if ('1' === ($modifiersData['enabled'] ?? '0')) {
|
||||
return [
|
||||
'form' => array_map(self::mapFormData(...), $modifiersData['form'] ?? []),
|
||||
'version' => 1,
|
||||
'enabled' => true,
|
||||
];
|
||||
}
|
||||
|
||||
return ['enabled' => false];
|
||||
}
|
||||
|
||||
private static function mapFormData(array|string $formData): array|string|null
|
||||
{
|
||||
if (is_array($formData) && array_key_exists('roll', $formData)) {
|
||||
return self::refactorRollingDate($formData);
|
||||
}
|
||||
|
||||
if (is_string($formData)) {
|
||||
// we try different date formats
|
||||
if (false !== \DateTimeImmutable::createFromFormat('d-m-Y', $formData)) {
|
||||
return $formData;
|
||||
}
|
||||
if (false !== \DateTimeImmutable::createFromFormat('Y-m-d', $formData)) {
|
||||
return $formData;
|
||||
}
|
||||
|
||||
// we try json content
|
||||
try {
|
||||
$data = json_decode($formData, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
if (is_array($data)) {
|
||||
if (array_key_exists('type', $data) && array_key_exists('id', $data) && in_array($data['type'], ['person', 'thirdParty', 'user'], true)) {
|
||||
return $data['id'];
|
||||
}
|
||||
$response = [];
|
||||
foreach ($data as $item) {
|
||||
if (array_key_exists('type', $item) && array_key_exists('id', $item) && in_array($item['type'], ['person', 'thirdParty', 'user'], true)) {
|
||||
$response[] = $item['id'];
|
||||
}
|
||||
}
|
||||
if ([] !== $response) {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
} catch (\JsonException) {
|
||||
return $formData;
|
||||
}
|
||||
}
|
||||
|
||||
return $formData;
|
||||
}
|
||||
|
||||
private static function refactorRollingDate(array $formData): ?array
|
||||
{
|
||||
if ('' === $formData['roll']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fixedDate = null !== ($formData['fixedDate'] ?? null) && '' !== $formData['fixedDate'] ?
|
||||
\DateTimeImmutable::createFromFormat('Y-m-d H:i:s', sprintf('%s 00:00:00', $formData['fixedDate']), new \DateTimeZone(date_default_timezone_get())) : null;
|
||||
|
||||
return (new RollingDate(
|
||||
$formData['roll'],
|
||||
$fixedDate,
|
||||
))->normalize();
|
||||
}
|
||||
}
|
@@ -37,12 +37,12 @@ interface ModifierInterface extends ExportElementInterface
|
||||
* @param QueryBuilder $qb the QueryBuilder initiated by the Export (and eventually modified by other Modifiers)
|
||||
* @param mixed[] $data the data from the Form (builded by buildForm)
|
||||
*/
|
||||
public function alterQuery(QueryBuilder $qb, $data);
|
||||
public function alterQuery(QueryBuilder $qb, $data, ExportGenerationContext $exportGenerationContext): void;
|
||||
|
||||
/**
|
||||
* On which type of Export this ModifiersInterface may apply.
|
||||
*
|
||||
* @return string the type on which the Modifiers apply
|
||||
*/
|
||||
public function applyOn();
|
||||
public function applyOn(): string;
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Export;
|
||||
|
||||
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final readonly class SortExportElement
|
||||
@@ -19,12 +20,21 @@ final readonly class SortExportElement
|
||||
private TranslatorInterface $translator,
|
||||
) {}
|
||||
|
||||
private function trans(string|TranslatableInterface $message): string
|
||||
{
|
||||
if ($message instanceof TranslatableInterface) {
|
||||
return $message->trans($this->translator, $this->translator->getLocale());
|
||||
}
|
||||
|
||||
return $this->translator->trans($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|string, FilterInterface> $elements
|
||||
*/
|
||||
public function sortFilters(array &$elements): void
|
||||
{
|
||||
uasort($elements, fn (FilterInterface $a, FilterInterface $b) => $this->translator->trans($a->getTitle()) <=> $this->translator->trans($b->getTitle()));
|
||||
uasort($elements, fn (FilterInterface $a, FilterInterface $b) => $this->trans($a->getTitle()) <=> $this->trans($b->getTitle()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,6 +42,6 @@ final readonly class SortExportElement
|
||||
*/
|
||||
public function sortAggregators(array &$elements): void
|
||||
{
|
||||
uasort($elements, fn (AggregatorInterface $a, AggregatorInterface $b) => $this->translator->trans($a->getTitle()) <=> $this->translator->trans($b->getTitle()));
|
||||
uasort($elements, fn (AggregatorInterface $a, AggregatorInterface $b) => $this->trans($a->getTitle()) <=> $this->trans($b->getTitle()));
|
||||
}
|
||||
}
|
||||
|
@@ -1,56 +0,0 @@
|
||||
<?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\Form\DataMapper;
|
||||
|
||||
use Chill\MainBundle\Entity\Regroupment;
|
||||
use Symfony\Component\Form\DataMapperInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
|
||||
final readonly class ExportPickCenterDataMapper implements DataMapperInterface
|
||||
{
|
||||
public function mapDataToForms($viewData, \Traversable $forms): void
|
||||
{
|
||||
if (null === $viewData) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var array<string, FormInterface> $form */
|
||||
$form = iterator_to_array($forms);
|
||||
|
||||
$form['center']->setData($viewData);
|
||||
|
||||
// NOTE: we do not map back the regroupments
|
||||
}
|
||||
|
||||
public function mapFormsToData(\Traversable $forms, &$viewData): void
|
||||
{
|
||||
/** @var array<string, FormInterface> $forms */
|
||||
$forms = iterator_to_array($forms);
|
||||
|
||||
$centers = [];
|
||||
|
||||
foreach ($forms['center']->getData() as $center) {
|
||||
$centers[spl_object_hash($center)] = $center;
|
||||
}
|
||||
|
||||
if (\array_key_exists('regroupment', $forms)) {
|
||||
/** @var Regroupment $regroupment */
|
||||
foreach ($forms['regroupment']->getData() as $regroupment) {
|
||||
foreach ($regroupment->getCenters() as $center) {
|
||||
$centers[spl_object_hash($center)] = $center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$viewData = array_values($centers);
|
||||
}
|
||||
}
|
@@ -13,15 +13,22 @@ namespace Chill\MainBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Entity\SavedExport;
|
||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||
use Chill\MainBundle\Form\Type\PickUserGroupOrUserDynamicType;
|
||||
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
class SavedExportType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly Security $security) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$savedExport = $options['data'];
|
||||
|
||||
$builder
|
||||
->add('title', TextType::class, [
|
||||
'required' => true,
|
||||
@@ -29,6 +36,14 @@ class SavedExportType extends AbstractType
|
||||
->add('description', ChillTextareaType::class, [
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
if ($this->security->isGranted(SavedExportVoter::SHARE, $savedExport)) {
|
||||
$builder->add('share', PickUserGroupOrUserDynamicType::class, [
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
'label' => 'saved_export.Share',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
|
@@ -66,8 +66,11 @@ class EntityToJsonTransformer implements DataTransformerInterface
|
||||
]);
|
||||
}
|
||||
|
||||
private function denormalizeOne(array $item)
|
||||
private function denormalizeOne(array|string $item)
|
||||
{
|
||||
if ('me' === $item) {
|
||||
return $item;
|
||||
}
|
||||
if (!\array_key_exists('type', $item)) {
|
||||
throw new TransformationFailedException('the key "type" is missing on element');
|
||||
}
|
||||
@@ -98,5 +101,6 @@ class EntityToJsonTransformer implements DataTransformerInterface
|
||||
'json',
|
||||
$context,
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -21,15 +21,18 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class AggregatorType extends AbstractType
|
||||
{
|
||||
public const ENABLED_FIELD = 'enabled';
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$exportManager = $options['export_manager'];
|
||||
$aggregator = $exportManager->getAggregator($options['aggregator_alias']);
|
||||
|
||||
$builder
|
||||
->add('enabled', CheckboxType::class, [
|
||||
->add(self::ENABLED_FIELD, CheckboxType::class, [
|
||||
'value' => true,
|
||||
'required' => false,
|
||||
'disabled' => $options['disable_enable_field'],
|
||||
]);
|
||||
|
||||
$aggregatorFormBuilder = $builder->create('form', FormType::class, [
|
||||
@@ -53,6 +56,7 @@ class AggregatorType extends AbstractType
|
||||
{
|
||||
$resolver->setRequired('aggregator_alias')
|
||||
->setRequired('export_manager')
|
||||
->setDefault('disable_enable_field', false)
|
||||
->setDefault('compound', true)
|
||||
->setDefault('error_bubbling', false);
|
||||
}
|
||||
|
@@ -35,7 +35,7 @@ class ExportType extends AbstractType
|
||||
public function __construct(
|
||||
private readonly ExportManager $exportManager,
|
||||
private readonly SortExportElement $sortExportElement,
|
||||
protected ParameterBagInterface $parameterBag,
|
||||
ParameterBagInterface $parameterBag,
|
||||
) {
|
||||
$this->personFieldsConfig = $parameterBag->get('chill_person.person_fields');
|
||||
}
|
||||
@@ -43,6 +43,8 @@ class ExportType extends AbstractType
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$export = $this->exportManager->getExport($options['export_alias']);
|
||||
/** @var bool $canEditFull */
|
||||
$canEditFull = $options['can_edit_full'];
|
||||
|
||||
$exportOptions = [
|
||||
'compound' => true,
|
||||
@@ -59,8 +61,18 @@ class ExportType extends AbstractType
|
||||
|
||||
if ($export instanceof \Chill\MainBundle\Export\ExportInterface) {
|
||||
// add filters
|
||||
$filters = $this->exportManager->getFiltersApplyingOn($export, $options['picked_centers']);
|
||||
$filterAliases = $options['allowed_filters'];
|
||||
$filters = [];
|
||||
if (is_iterable($filterAliases)) {
|
||||
foreach ($filterAliases as $alias => $filter) {
|
||||
$filters[$alias] = $filter;
|
||||
}
|
||||
} else {
|
||||
$filters = $this->exportManager->getFiltersApplyingOn($export, $options['picked_centers']);
|
||||
}
|
||||
|
||||
$this->sortExportElement->sortFilters($filters);
|
||||
|
||||
$filterBuilder = $builder->create(self::FILTER_KEY, FormType::class, ['compound' => true]);
|
||||
|
||||
foreach ($filters as $alias => $filter) {
|
||||
@@ -70,15 +82,26 @@ class ExportType extends AbstractType
|
||||
'constraints' => [
|
||||
new ExportElementConstraint(['element' => $filter]),
|
||||
],
|
||||
'disable_enable_field' => !$canEditFull,
|
||||
]);
|
||||
}
|
||||
|
||||
$builder->add($filterBuilder);
|
||||
|
||||
// add aggregators
|
||||
$aggregators = $this->exportManager
|
||||
->getAggregatorsApplyingOn($export, $options['picked_centers']);
|
||||
$aggregatorsAliases = $options['allowed_aggregators'];
|
||||
$aggregators = [];
|
||||
if (is_iterable($aggregatorsAliases)) {
|
||||
foreach ($aggregatorsAliases as $alias => $aggregator) {
|
||||
$aggregators[$alias] = $aggregator;
|
||||
}
|
||||
} else {
|
||||
$aggregators = $this->exportManager
|
||||
->getAggregatorsApplyingOn($export, $options['picked_centers']);
|
||||
}
|
||||
|
||||
$this->sortExportElement->sortAggregators($aggregators);
|
||||
|
||||
$aggregatorBuilder = $builder->create(
|
||||
self::AGGREGATOR_KEY,
|
||||
FormType::class,
|
||||
@@ -96,11 +119,11 @@ class ExportType extends AbstractType
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$aggregatorBuilder->add($alias, AggregatorType::class, [
|
||||
'aggregator_alias' => $alias,
|
||||
'export_manager' => $this->exportManager,
|
||||
'label' => $aggregator->getTitle(),
|
||||
'disable_enable_field' => !$canEditFull,
|
||||
'constraints' => [
|
||||
new ExportElementConstraint(['element' => $aggregator]),
|
||||
],
|
||||
@@ -125,8 +148,13 @@ class ExportType extends AbstractType
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setRequired(['export_alias', 'picked_centers'])
|
||||
$resolver->setRequired(['export_alias', 'picked_centers', 'can_edit_full'])
|
||||
->setAllowedTypes('export_alias', ['string'])
|
||||
->setAllowedValues('can_edit_full', [true, false])
|
||||
->setDefault('allowed_filters', null)
|
||||
->setAllowedTypes('allowed_filters', ['iterable', 'null'])
|
||||
->setDefault('allowed_aggregators', null)
|
||||
->setAllowedTypes('allowed_aggregators', ['iterable', 'null'])
|
||||
->setDefault('compound', true)
|
||||
->setDefault('constraints', [
|
||||
// new \Chill\MainBundle\Validator\Constraints\Export\ExportElementConstraint()
|
||||
|
@@ -34,6 +34,7 @@ class FilterType extends AbstractType
|
||||
->add(self::ENABLED_FIELD, CheckboxType::class, [
|
||||
'value' => true,
|
||||
'required' => false,
|
||||
'disabled' => $options['disable_enable_field'],
|
||||
]);
|
||||
|
||||
$filterFormBuilder = $builder->create('form', FormType::class, [
|
||||
@@ -58,6 +59,7 @@ class FilterType extends AbstractType
|
||||
$resolver
|
||||
->setRequired('filter')
|
||||
->setAllowedTypes('filter', [FilterInterface::class])
|
||||
->setDefault('disable_enable_field', false)
|
||||
->setDefault('compound', true)
|
||||
->setDefault('error_bubbling', false);
|
||||
}
|
||||
|
@@ -14,9 +14,9 @@ namespace Chill\MainBundle\Form\Type\Export;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\Regroupment;
|
||||
use Chill\MainBundle\Export\ExportManager;
|
||||
use Chill\MainBundle\Form\DataMapper\ExportPickCenterDataMapper;
|
||||
use Chill\MainBundle\Repository\RegroupmentRepository;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
|
||||
use Chill\MainBundle\Service\Regroupement\RegroupementFiltering;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
@@ -27,27 +27,26 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
*/
|
||||
final class PickCenterType extends AbstractType
|
||||
{
|
||||
public const CENTERS_IDENTIFIERS = 'c';
|
||||
|
||||
public function __construct(
|
||||
private readonly ExportManager $exportManager,
|
||||
private readonly RegroupmentRepository $regroupmentRepository,
|
||||
private readonly AuthorizationHelperForCurrentUserInterface $authorizationHelper,
|
||||
private readonly RegroupementFiltering $regroupementFiltering,
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$export = $this->exportManager->getExport($options['export_alias']);
|
||||
$centers = $this->authorizationHelper->getReachableCenters(
|
||||
$export->requiredRole()
|
||||
$export->requiredRole(),
|
||||
);
|
||||
|
||||
$centersActive = array_filter($centers, fn (Center $c) => $c->getIsActive());
|
||||
|
||||
// order alphabetically
|
||||
usort($centersActive, fn (Center $a, Center $b) => $a->getCenter() <=> $b->getName());
|
||||
usort($centersActive, fn (Center $a, Center $b) => $a->getName() <=> $b->getName());
|
||||
|
||||
$builder->add('center', EntityType::class, [
|
||||
$builder->add('centers', EntityType::class, [
|
||||
'class' => Center::class,
|
||||
'choices' => $centersActive,
|
||||
'label' => 'center',
|
||||
@@ -56,18 +55,22 @@ final class PickCenterType extends AbstractType
|
||||
'choice_label' => static fn (Center $c) => $c->getName(),
|
||||
]);
|
||||
|
||||
if (\count($this->regroupmentRepository->findAllActive()) > 0) {
|
||||
$builder->add('regroupment', EntityType::class, [
|
||||
$groups = $this->regroupementFiltering
|
||||
->filterContainsAtLeastOneCenter($this->regroupmentRepository->findAllActive(), $centersActive);
|
||||
|
||||
// order alphabetically
|
||||
usort($groups, fn (Regroupment $a, Regroupment $b) => $a->getName() <=> $b->getName());
|
||||
|
||||
if (\count($groups) > 0) {
|
||||
$builder->add('regroupments', EntityType::class, [
|
||||
'class' => Regroupment::class,
|
||||
'label' => 'regroupment',
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'choices' => $this->regroupmentRepository->findAllActive(),
|
||||
'choices' => $groups,
|
||||
'choice_label' => static fn (Regroupment $r) => $r->getName(),
|
||||
]);
|
||||
}
|
||||
|
||||
$builder->setDataMapper(new ExportPickCenterDataMapper());
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
|
@@ -0,0 +1,82 @@
|
||||
<?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\Form\Type;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Form\Type\DataTransformer\EntityToJsonTransformer;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
/**
|
||||
* Pick user dymically, using vuejs module "AddPerson".
|
||||
*
|
||||
* Possible options:
|
||||
*
|
||||
* - `multiple`: pick one or more users
|
||||
* - `suggested`: a list of suggested users
|
||||
* - `suggest_myself`: append the current user to the list of suggested
|
||||
* - `as_id`: only the id will be set in the returned data
|
||||
* - `submit_on_adding_new_entity`: the browser will immediately submit the form when new users are checked
|
||||
*/
|
||||
class PickUserOrMeDynamicType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DenormalizerInterface $denormalizer,
|
||||
private readonly SerializerInterface $serializer,
|
||||
private readonly NormalizerInterface $normalizer,
|
||||
) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->addViewTransformer(new EntityToJsonTransformer($this->denormalizer, $this->serializer, $options['multiple'], 'user'));
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||
{
|
||||
$view->vars['multiple'] = $options['multiple'];
|
||||
$view->vars['types'] = ['user'];
|
||||
$view->vars['uniqid'] = uniqid('pick_user_or_me_dyn');
|
||||
$view->vars['suggested'] = [];
|
||||
$view->vars['as_id'] = true === $options['as_id'] ? '1' : '0';
|
||||
$view->vars['submit_on_adding_new_entity'] = true === $options['submit_on_adding_new_entity'] ? '1' : '0';
|
||||
|
||||
foreach ($options['suggested'] as $user) {
|
||||
$view->vars['suggested'][] = $this->normalizer->normalize($user, 'json', ['groups' => 'read']);
|
||||
}
|
||||
// $user = /* should come from context */ $options['context'];
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver
|
||||
->setDefault('multiple', false)
|
||||
->setAllowedTypes('multiple', ['bool'])
|
||||
->setDefault('compound', false)
|
||||
->setDefault('suggested', [])
|
||||
// if set to true, only the id will be set inside the content. The denormalization will not work.
|
||||
->setDefault('as_id', false)
|
||||
->setAllowedTypes('as_id', ['bool'])
|
||||
->setDefault('submit_on_adding_new_entity', false)
|
||||
->setAllowedTypes('submit_on_adding_new_entity', ['bool']);
|
||||
}
|
||||
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return 'pick_entity_dynamic';
|
||||
}
|
||||
}
|
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
@@ -23,6 +24,9 @@ class UserGroupType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
/** @var UserGroup $userGroup */
|
||||
$userGroup = $options['data'];
|
||||
|
||||
$builder
|
||||
->add('label', TranslatableStringFormType::class, [
|
||||
'label' => 'user_group.Label',
|
||||
@@ -46,20 +50,25 @@ class UserGroupType extends AbstractType
|
||||
'help' => 'user_group.ExcludeKeyHelp',
|
||||
'required' => false,
|
||||
'empty_data' => '',
|
||||
])
|
||||
->add('users', PickUserDynamicType::class, [
|
||||
'label' => 'user_group.Users',
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
'empty_data' => [],
|
||||
])
|
||||
->add('adminUsers', PickUserDynamicType::class, [
|
||||
'label' => 'user_group.adminUsers',
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
'empty_data' => [],
|
||||
'help' => 'user_group.adminUsersHelp',
|
||||
])
|
||||
;
|
||||
]);
|
||||
|
||||
if (!$userGroup->hasUserJob()) {
|
||||
$builder
|
||||
->add('users', PickUserDynamicType::class, [
|
||||
'label' => 'user_group.Users',
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
'empty_data' => [],
|
||||
])
|
||||
->add('adminUsers', PickUserDynamicType::class, [
|
||||
'label' => 'user_group.adminUsers',
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
'empty_data' => [],
|
||||
'help' => 'user_group.adminUsersHelp',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,85 @@
|
||||
<?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\Repository;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||
use Chill\MainBundle\Entity\ExportGeneration;
|
||||
use Chill\MainBundle\Entity\SavedExport;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<ExportGeneration>
|
||||
*
|
||||
* @implements AssociatedEntityToStoredObjectInterface<ExportGeneration>
|
||||
*/
|
||||
class ExportGenerationRepository extends ServiceEntityRepository implements AssociatedEntityToStoredObjectInterface
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, ExportGeneration::class);
|
||||
}
|
||||
|
||||
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?ExportGeneration
|
||||
{
|
||||
return $this->createQueryBuilder('e')
|
||||
->where('e.storedObject = :storedObject')
|
||||
->setParameter('storedObject', $storedObject)
|
||||
->getQuery()
|
||||
->getOneOrNullResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<ExportGeneration>
|
||||
*/
|
||||
public function findExportGenerationByAliasAndUser(string $alias, User $user, int $limit = 100, int $offset = 0): array
|
||||
{
|
||||
return $this->createQueryBuilder('e')
|
||||
->where('e.createdBy = :user')
|
||||
->andWhere('e.exportAlias LIKE :alias')
|
||||
->orderBy('e.createdAt', 'DESC')
|
||||
->setParameter('user', $user)
|
||||
->setParameter('alias', $alias)
|
||||
->setFirstResult($offset)
|
||||
->setMaxResults($limit)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<ExportGeneration>
|
||||
*/
|
||||
public function findExportGenerationBySavedExportAndUser(SavedExport $savedExport, User $user, int $limit = 100, int $offset = 0): array
|
||||
{
|
||||
return $this->createQueryBuilder('e')
|
||||
->where('e.createdBy = :user')
|
||||
->andWhere('e.savedExport = :savedExport')
|
||||
->orderBy('e.createdAt', 'DESC')
|
||||
->setParameter('user', $user)
|
||||
->setParameter('savedExport', $savedExport)
|
||||
->setFirstResult($offset)
|
||||
->setMaxResults($limit)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
public function findExpiredExportGeneration(\DateTimeImmutable $atDate): iterable
|
||||
{
|
||||
return $this->createQueryBuilder('e')
|
||||
->where('e.deleteAt < :atDate')
|
||||
->setParameter('atDate', $atDate)
|
||||
->getQuery()
|
||||
->toIterable();
|
||||
}
|
||||
}
|
@@ -13,6 +13,7 @@ namespace Chill\MainBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\Address;
|
||||
use Chill\MainBundle\Entity\GeographicalUnit;
|
||||
use Chill\MainBundle\Entity\GeographicalUnit\SimpleGeographicalUnitDTO;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
@@ -42,7 +43,7 @@ final readonly class GeographicalUnitRepository implements GeographicalUnitRepos
|
||||
$qb = $this->buildQueryGeographicalUnitContainingAddress($address);
|
||||
|
||||
return $qb
|
||||
->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', GeographicalUnit\SimpleGeographicalUnitDTO::class))
|
||||
->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', SimpleGeographicalUnitDTO::class))
|
||||
->addOrderBy('IDENTITY(gu.layer)')
|
||||
->addOrderBy('gu.unitName')
|
||||
->getQuery()
|
||||
@@ -58,7 +59,7 @@ final readonly class GeographicalUnitRepository implements GeographicalUnitRepos
|
||||
;
|
||||
|
||||
return $qb
|
||||
->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', GeographicalUnit\SimpleGeographicalUnitDTO::class))
|
||||
->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', SimpleGeographicalUnitDTO::class))
|
||||
->innerJoin(Address::class, 'address', Join::WITH, 'ST_CONTAINS(gu.geom, address.point) = TRUE')
|
||||
->where($qb->expr()->eq('address', ':address'))
|
||||
->setParameter('address', $address)
|
||||
@@ -70,6 +71,19 @@ final readonly class GeographicalUnitRepository implements GeographicalUnitRepos
|
||||
return $this->repository->find($id);
|
||||
}
|
||||
|
||||
public function findSimpleGeographicalUnit(int $id): ?SimpleGeographicalUnitDTO
|
||||
{
|
||||
$qb = $this->repository
|
||||
->createQueryBuilder('gu');
|
||||
|
||||
return $qb
|
||||
->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', SimpleGeographicalUnitDTO::class))
|
||||
->where('gu.id = :id')
|
||||
->setParameter('id', $id)
|
||||
->getQuery()
|
||||
->getOneOrNullResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Will return only partial object, where the @see{GeographicalUnit::geom} property is not loaded.
|
||||
*
|
||||
@@ -79,7 +93,7 @@ final readonly class GeographicalUnitRepository implements GeographicalUnitRepos
|
||||
{
|
||||
return $this->repository
|
||||
->createQueryBuilder('gu')
|
||||
->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', GeographicalUnit\SimpleGeographicalUnitDTO::class))
|
||||
->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', SimpleGeographicalUnitDTO::class))
|
||||
->addOrderBy('IDENTITY(gu.layer)')
|
||||
->addOrderBy('gu.unitName')
|
||||
->getQuery()
|
||||
|
@@ -27,4 +27,6 @@ interface GeographicalUnitRepositoryInterface extends ObjectRepository
|
||||
public function findGeographicalUnitContainingAddress(Address $address, int $offset = 0, int $limit = 50): array;
|
||||
|
||||
public function countGeographicalUnitContainingAddress(Address $address): int;
|
||||
|
||||
public function findSimpleGeographicalUnit(int $id): ?SimpleGeographicalUnitDTO;
|
||||
}
|
||||
|
@@ -16,9 +16,8 @@ use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
use Doctrine\ORM\NoResultException;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
final readonly class RegroupmentRepository implements ObjectRepository
|
||||
final readonly class RegroupmentRepository implements RegroupmentRepositoryInterface
|
||||
{
|
||||
private EntityRepository $repository;
|
||||
|
||||
|
@@ -0,0 +1,34 @@
|
||||
<?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\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\Regroupment;
|
||||
use Doctrine\ORM\NonUniqueResultException;
|
||||
use Doctrine\ORM\NoResultException;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
/**
|
||||
* @template-extends ObjectRepository<Regroupment>
|
||||
*/
|
||||
interface RegroupmentRepositoryInterface extends ObjectRepository
|
||||
{
|
||||
/**
|
||||
* @throws NonUniqueResultException
|
||||
* @throws NoResultException
|
||||
*/
|
||||
public function findOneByName(string $name): ?Regroupment;
|
||||
|
||||
/**
|
||||
* @return array<Regroupment>
|
||||
*/
|
||||
public function findRegroupmentAssociatedToNoCenter(): array;
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
<?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\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\ExportGeneration;
|
||||
use Chill\MainBundle\Entity\SavedExport;
|
||||
|
||||
readonly class SavedExportOrExportGenerationRepository
|
||||
{
|
||||
public function __construct(
|
||||
private SavedExportRepositoryInterface $savedExportRepository,
|
||||
private ExportGenerationRepository $exportGenerationRepository,
|
||||
) {}
|
||||
|
||||
public function findById(string $uuid): SavedExport|ExportGeneration|null
|
||||
{
|
||||
if (null !== $savedExport = $this->savedExportRepository->find($uuid)) {
|
||||
return $savedExport;
|
||||
}
|
||||
|
||||
return $this->exportGenerationRepository->find($uuid);
|
||||
}
|
||||
}
|
@@ -13,9 +13,12 @@ namespace Chill\MainBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\SavedExport;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use Symfony\Component\String\UnicodeString;
|
||||
|
||||
/**
|
||||
* @implements ObjectRepository<SavedExport>
|
||||
@@ -55,6 +58,51 @@ class SavedExportRepository implements SavedExportRepositoryInterface
|
||||
->where($qb->expr()->eq('se.user', ':user'))
|
||||
->setParameter('user', $user);
|
||||
|
||||
return $this->prepareResult($qb, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
||||
public function findSharedWithUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null, array $filters = []): array
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('se');
|
||||
|
||||
$qb
|
||||
->where(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->eq('se.user', ':user'),
|
||||
$qb->expr()->isMemberOf(':user', 'se.sharedWithUsers'),
|
||||
$qb->expr()->exists(
|
||||
sprintf('SELECT 1 FROM %s ug WHERE ug MEMBER OF se.sharedWithGroups AND :user MEMBER OF ug.users', UserGroup::class)
|
||||
)
|
||||
)
|
||||
)
|
||||
->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);
|
||||
}
|
||||
|
||||
private function prepareResult(QueryBuilder $qb, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array
|
||||
{
|
||||
if (null !== $limit) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
|
@@ -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,6 +37,15 @@ interface SavedExportRepositoryInterface extends ObjectRepository
|
||||
*/
|
||||
public function findByUser(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 getClassName(): string;
|
||||
|
@@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\UserGroup;
|
||||
use Chill\MainBundle\Entity\UserJob;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
@@ -30,9 +31,6 @@ readonly class UserJobRepository implements UserJobRepositoryInterface
|
||||
return $this->repository->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|UserJob[]
|
||||
*/
|
||||
public function findAll(): array
|
||||
{
|
||||
return $this->repository->findAll();
|
||||
@@ -56,12 +54,20 @@ readonly class UserJobRepository implements UserJobRepositoryInterface
|
||||
return $jobs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed|null $limit
|
||||
* @param mixed|null $offset
|
||||
*
|
||||
* @return array|object[]|UserJob[]
|
||||
*/
|
||||
public function findAllNotAssociatedWithUserGroup(): array
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('u');
|
||||
$qb->select('u');
|
||||
|
||||
$qb->where(
|
||||
$qb->expr()->not(
|
||||
$qb->expr()->exists(sprintf('SELECT 1 FROM %s ug WHERE ug.userJob = u', UserGroup::class))
|
||||
)
|
||||
);
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null)
|
||||
{
|
||||
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
|
@@ -14,18 +14,15 @@ namespace Chill\MainBundle\Repository;
|
||||
use Chill\MainBundle\Entity\UserJob;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
/**
|
||||
* @template-extends ObjectRepository<UserJob>
|
||||
*/
|
||||
interface UserJobRepositoryInterface extends ObjectRepository
|
||||
{
|
||||
public function find($id): ?UserJob;
|
||||
|
||||
/**
|
||||
* @return array|UserJob[]
|
||||
*/
|
||||
public function findAll(): array;
|
||||
|
||||
/**
|
||||
* @return array|UserJob[]
|
||||
*/
|
||||
public function findAllActive(): array;
|
||||
|
||||
/**
|
||||
@@ -36,11 +33,14 @@ interface UserJobRepositoryInterface extends ObjectRepository
|
||||
public function findAllOrderedByName(): array;
|
||||
|
||||
/**
|
||||
* @param mixed|null $limit
|
||||
* @param mixed|null $offset
|
||||
* Find all the user job which are not related to a UserGroup.
|
||||
*
|
||||
* @return array|object[]|UserJob[]
|
||||
* This is useful for synchronizing UserGroups with jobs.
|
||||
*
|
||||
* @return list<UserJob>
|
||||
*/
|
||||
public function findAllNotAssociatedWithUserGroup(): array;
|
||||
|
||||
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null);
|
||||
|
||||
public function findOneBy(array $criteria): ?UserJob;
|
||||
|
@@ -0,0 +1,18 @@
|
||||
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
|
||||
import { ExportGeneration } from "ChillMainAssets/types";
|
||||
|
||||
export const fetchExportGenerationStatus = async (
|
||||
exportGenerationId: string,
|
||||
): Promise<ExportGeneration> =>
|
||||
makeFetch(
|
||||
"GET",
|
||||
`/api/1.0/main/export-generation/${exportGenerationId}/object`,
|
||||
);
|
||||
|
||||
export const generateFromSavedExport = async (
|
||||
savedExportUuid: string,
|
||||
): Promise<ExportGeneration> =>
|
||||
makeFetch(
|
||||
"POST",
|
||||
`/api/1.0/main/export/export-generation/create-from-saved-export/${savedExportUuid}`,
|
||||
);
|
@@ -0,0 +1,3 @@
|
||||
export function buildReturnPath(location: Location): string {
|
||||
return location.pathname + location.search;
|
||||
}
|
@@ -12,6 +12,11 @@ function loadDynamicPicker(element) {
|
||||
let apps = element.querySelectorAll('[data-module="pick-dynamic"]');
|
||||
|
||||
apps.forEach(function (el) {
|
||||
let suggested;
|
||||
let as_id;
|
||||
let submit_on_adding_new_entity;
|
||||
let label;
|
||||
let isCurrentUserPicker;
|
||||
const isMultiple = parseInt(el.dataset.multiple) === 1,
|
||||
uniqId = el.dataset.uniqid,
|
||||
input = element.querySelector(
|
||||
@@ -22,12 +27,13 @@ function loadDynamicPicker(element) {
|
||||
? JSON.parse(input.value)
|
||||
: input.value === "[]" || input.value === ""
|
||||
? null
|
||||
: [JSON.parse(input.value)],
|
||||
suggested = JSON.parse(el.dataset.suggested),
|
||||
as_id = parseInt(el.dataset.asId) === 1,
|
||||
submit_on_adding_new_entity =
|
||||
parseInt(el.dataset.submitOnAddingNewEntity) === 1,
|
||||
label = el.dataset.label;
|
||||
: [JSON.parse(input.value)];
|
||||
suggested = JSON.parse(el.dataset.suggested);
|
||||
as_id = parseInt(el.dataset.asId) === 1;
|
||||
submit_on_adding_new_entity =
|
||||
parseInt(el.dataset.submitOnAddingNewEntity) === 1;
|
||||
label = el.dataset.label;
|
||||
isCurrentUserPicker = uniqId.startsWith("pick_user_or_me_dyn");
|
||||
|
||||
if (!isMultiple) {
|
||||
if (input.value === "[]") {
|
||||
@@ -44,6 +50,7 @@ function loadDynamicPicker(element) {
|
||||
':uniqid="uniqid" ' +
|
||||
':suggested="notPickedSuggested" ' +
|
||||
':label="label" ' +
|
||||
':isCurrentUserPicker="isCurrentUserPicker" ' +
|
||||
'@addNewEntity="addNewEntity" ' +
|
||||
'@removeEntity="removeEntity" ' +
|
||||
'@addNewEntityProcessEnded="addNewEntityProcessEnded"' +
|
||||
@@ -61,6 +68,7 @@ function loadDynamicPicker(element) {
|
||||
as_id,
|
||||
submit_on_adding_new_entity,
|
||||
label,
|
||||
isCurrentUserPicker,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -89,7 +97,8 @@ function loadDynamicPicker(element) {
|
||||
const ids = this.picked.map((el) => el.id);
|
||||
input.value = ids.join(",");
|
||||
}
|
||||
console.log(entity);
|
||||
console.log(this.picked);
|
||||
// console.log(entity);
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
|
@@ -1,14 +0,0 @@
|
||||
import { download_report } from "../../lib/download-report/download-report";
|
||||
|
||||
window.addEventListener("DOMContentLoaded", function (e) {
|
||||
const export_generate_url = window.export_generate_url;
|
||||
|
||||
if (typeof export_generate_url === "undefined") {
|
||||
console.error("Alias not found!");
|
||||
throw new Error("Alias not found!");
|
||||
}
|
||||
|
||||
const query = window.location.search,
|
||||
container = document.querySelector("#download_container");
|
||||
download_report(export_generate_url + query.toString(), container);
|
||||
});
|
@@ -1,4 +1,5 @@
|
||||
import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
|
||||
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
|
||||
|
||||
export interface DateTime {
|
||||
datetime: string;
|
||||
@@ -201,6 +202,16 @@ export interface WorkflowAttachment {
|
||||
genericDoc: null | GenericDoc;
|
||||
}
|
||||
|
||||
export interface ExportGeneration {
|
||||
id: string;
|
||||
type: "export_generation";
|
||||
exportAlias: string;
|
||||
createdBy: User | null;
|
||||
createdAt: DateTime | null;
|
||||
status: StoredObjectStatus;
|
||||
storedObject: StoredObject;
|
||||
}
|
||||
|
||||
export interface PrivateCommentEmbeddable {
|
||||
comments: Record<number, string>;
|
||||
}
|
||||
|
@@ -0,0 +1,141 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
trans,
|
||||
EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING,
|
||||
EXPORT_GENERATION_TOO_MANY_RETRIES,
|
||||
EXPORT_GENERATION_ERROR_WHILE_GENERATING_EXPORT,
|
||||
EXPORT_GENERATION_EXPORT_READY,
|
||||
} from "translator";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
|
||||
import { fetchExportGenerationStatus } from "ChillMainAssets/lib/api/export";
|
||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
|
||||
import { ExportGeneration } from "ChillMainAssets/types";
|
||||
|
||||
interface AppProps {
|
||||
exportGenerationId: string;
|
||||
title: string;
|
||||
createdDate: string;
|
||||
}
|
||||
|
||||
const props = defineProps<AppProps>();
|
||||
|
||||
const exportGeneration = ref<ExportGeneration | null>(null);
|
||||
|
||||
const status = computed<StoredObjectStatus>(
|
||||
() => exportGeneration.value?.status ?? "pending",
|
||||
);
|
||||
const storedObject = computed<null | StoredObject>(() => {
|
||||
if (exportGeneration.value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return exportGeneration.value?.storedObject;
|
||||
});
|
||||
|
||||
const isPending = computed<boolean>(() => status.value === "pending");
|
||||
const isFetching = computed<boolean>(
|
||||
() => tryiesForReady.value < maxTryiesForReady,
|
||||
);
|
||||
const isReady = computed<boolean>(() => status.value === "ready");
|
||||
const isFailure = computed<boolean>(() => status.value === "failure");
|
||||
const filename = computed<string>(() => `${props.title}-${props.createdDate}`);
|
||||
|
||||
/**
|
||||
* counter for the number of times that we check for a new status
|
||||
*/
|
||||
let tryiesForReady = ref<number>(0);
|
||||
|
||||
/**
|
||||
* how many times we may check for a new status, once loaded
|
||||
*/
|
||||
const maxTryiesForReady = 120;
|
||||
|
||||
const checkForReady = function (): void {
|
||||
if (
|
||||
"ready" === status.value ||
|
||||
"empty" === status.value ||
|
||||
"failure" === status.value ||
|
||||
// stop reloading if the page stays opened for a long time
|
||||
tryiesForReady.value > maxTryiesForReady
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
tryiesForReady.value = tryiesForReady.value + 1;
|
||||
setTimeout(onObjectNewStatusCallback, 5000);
|
||||
};
|
||||
|
||||
const onObjectNewStatusCallback = async function (): Promise<void> {
|
||||
exportGeneration.value = await fetchExportGenerationStatus(
|
||||
props.exportGenerationId,
|
||||
);
|
||||
|
||||
if (isPending.value) {
|
||||
checkForReady();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
onObjectNewStatusCallback();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="waiting-screen">
|
||||
<div
|
||||
v-if="isPending && isFetching"
|
||||
class="alert alert-danger text-center"
|
||||
>
|
||||
<div>
|
||||
<p>
|
||||
{{ trans(EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<i class="fa fa-cog fa-spin fa-3x fa-fw"></i>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isPending && !isFetching" class="alert alert-info">
|
||||
<div>
|
||||
<p>
|
||||
{{ trans(EXPORT_GENERATION_TOO_MANY_RETRIES) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isFailure" class="alert alert-danger text-center">
|
||||
<div>
|
||||
<p>
|
||||
{{ trans(EXPORT_GENERATION_ERROR_WHILE_GENERATING_EXPORT) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isReady" class="alert alert-success text-center">
|
||||
<div>
|
||||
<p>
|
||||
{{ trans(EXPORT_GENERATION_EXPORT_READY) }}
|
||||
</p>
|
||||
|
||||
<p v-if="storedObject !== null">
|
||||
<document-action-buttons-group
|
||||
:stored-object="storedObject"
|
||||
:filename="filename"
|
||||
></document-action-buttons-group>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
#waiting-screen {
|
||||
> .alert {
|
||||
min-height: 350px;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,15 @@
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
|
||||
const el = document.getElementById("app");
|
||||
|
||||
if (null === el) {
|
||||
console.error("div element app was not found");
|
||||
throw new Error("div element app was not found");
|
||||
}
|
||||
|
||||
const exportGenerationId = el?.dataset.exportGenerationId as string;
|
||||
const title = el?.dataset.exportTitle as string;
|
||||
const createdDate = el?.dataset.exportGenerationDate as string;
|
||||
|
||||
createApp(App, { exportGenerationId, title, createdDate }).mount(el);
|
@@ -1,10 +1,26 @@
|
||||
<template>
|
||||
<ul :class="listClasses" v-if="picked.length && displayPicked">
|
||||
<li v-for="p in picked" @click="removeEntity(p)" :key="p.type + p.id">
|
||||
<span class="chill_denomination">{{ p.text }}</span>
|
||||
<span
|
||||
v-if="'me' === p"
|
||||
class="chill_denomination current-user updatedBy"
|
||||
>{{ trans(USER_CURRENT_USER) }}</span
|
||||
>
|
||||
<span v-else class="chill_denomination">{{ p.text }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="record_actions">
|
||||
<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"
|
||||
/>
|
||||
{{ trans(USER_CURRENT_USER) }}
|
||||
</label>
|
||||
</li>
|
||||
<li class="add-persons">
|
||||
<add-persons
|
||||
:options="addPersonsOptions"
|
||||
@@ -24,119 +40,83 @@
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue";
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue"; // eslint-disable-line
|
||||
import { appMessages } from "./i18n";
|
||||
import { trans, USER_CURRENT_USER } from "translator";
|
||||
|
||||
export default {
|
||||
name: "PickEntity",
|
||||
props: {
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
types: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
picked: {
|
||||
required: true,
|
||||
},
|
||||
uniqid: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
removableIfSet: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
displayPicked: {
|
||||
// display picked entities.
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
suggested: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
emits: ["addNewEntity", "removeEntity", "addNewEntityProcessEnded"],
|
||||
components: {
|
||||
AddPersons,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
key: "",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
addPersonsOptions() {
|
||||
return {
|
||||
uniq: !this.multiple,
|
||||
type: this.types,
|
||||
priority: null,
|
||||
button: {
|
||||
size: "btn-sm",
|
||||
class: "btn-submit",
|
||||
},
|
||||
};
|
||||
},
|
||||
translatedListOfTypes() {
|
||||
if (this.label !== "") {
|
||||
return this.label;
|
||||
}
|
||||
const props = defineProps({
|
||||
multiple: Boolean,
|
||||
types: Array,
|
||||
picked: Array,
|
||||
uniqid: String,
|
||||
removableIfSet: { type: Boolean, default: true },
|
||||
displayPicked: { type: Boolean, default: true },
|
||||
suggested: { type: Array, default: () => [] },
|
||||
label: String,
|
||||
isCurrentUserPicker: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
let trans = [];
|
||||
this.types.forEach((t) => {
|
||||
if (this.$props.multiple) {
|
||||
trans.push(appMessages.fr.pick_entity[t].toLowerCase());
|
||||
} else {
|
||||
trans.push(
|
||||
appMessages.fr.pick_entity[t + "_one"].toLowerCase(),
|
||||
);
|
||||
}
|
||||
});
|
||||
const emit = defineEmits([
|
||||
"addNewEntity",
|
||||
"removeEntity",
|
||||
"addNewEntityProcessEnded",
|
||||
]);
|
||||
|
||||
if (this.$props.multiple) {
|
||||
return (
|
||||
appMessages.fr.pick_entity.modal_title + trans.join(", ")
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
appMessages.fr.pick_entity.modal_title_one +
|
||||
trans.join(", ")
|
||||
);
|
||||
}
|
||||
},
|
||||
listClasses() {
|
||||
return {
|
||||
"list-suggest": true,
|
||||
"remove-items": this.$props.removableIfSet,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addNewSuggested(entity) {
|
||||
this.$emit("addNewEntity", { entity: entity });
|
||||
},
|
||||
addNewEntity({ selected, modal }) {
|
||||
selected.forEach((item) => {
|
||||
this.$emit("addNewEntity", { entity: item.result });
|
||||
}, this);
|
||||
this.$refs.addPersons.resetSearch(); // to cast child method
|
||||
modal.showModal = false;
|
||||
this.$emit("addNewEntityProcessEnded");
|
||||
},
|
||||
removeEntity(entity) {
|
||||
if (!this.$props.removableIfSet) {
|
||||
return;
|
||||
}
|
||||
this.$emit("removeEntity", { entity: entity });
|
||||
},
|
||||
},
|
||||
const itsMeCheckbox = ref(null);
|
||||
const addPersons = ref(null);
|
||||
|
||||
const addPersonsOptions = computed(() => ({
|
||||
uniq: !props.multiple,
|
||||
type: props.types,
|
||||
priority: null,
|
||||
button: { size: "btn-sm", class: "btn-submit" },
|
||||
}));
|
||||
|
||||
const translatedListOfTypes = computed(() => {
|
||||
if (props.label) return props.label;
|
||||
let trans = props.types.map((t) =>
|
||||
props.multiple
|
||||
? appMessages.fr.pick_entity[t].toLowerCase()
|
||||
: appMessages.fr.pick_entity[t + "_one"].toLowerCase(),
|
||||
);
|
||||
return props.multiple
|
||||
? appMessages.fr.pick_entity.modal_title + trans.join(", ")
|
||||
: appMessages.fr.pick_entity.modal_title_one + trans.join(", ");
|
||||
});
|
||||
|
||||
const listClasses = computed(() => ({
|
||||
"list-suggest": true,
|
||||
"remove-items": props.removableIfSet,
|
||||
}));
|
||||
|
||||
const selectItsMe = (event) =>
|
||||
event.target.checked ? addNewSuggested("me") : removeEntity("me");
|
||||
|
||||
const addNewSuggested = (entity) => {
|
||||
emit("addNewEntity", { entity });
|
||||
};
|
||||
|
||||
const addNewEntity = ({ selected, modal }) => {
|
||||
selected.forEach((item) => emit("addNewEntity", { entity: item.result }));
|
||||
addPersons.value?.resetSearch();
|
||||
modal.showModal = false;
|
||||
emit("addNewEntityProcessEnded");
|
||||
};
|
||||
|
||||
const removeEntity = (entity) => {
|
||||
if (!props.removableIfSet) return;
|
||||
if (entity === "me" && itsMeCheckbox.value) {
|
||||
itsMeCheckbox.value.checked = false;
|
||||
}
|
||||
emit("removeEntity", { entity });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.current-user {
|
||||
color: var(--bs-body-color);
|
||||
background-color: var(--bs-chill-l-gray) !important;
|
||||
}
|
||||
</style>
|
||||
|
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import GenerateButton from "ChillMainAssets/vuejs/SavedExportButtons/Component/GenerateButton.vue";
|
||||
|
||||
interface SavedExportButtonsConfig {
|
||||
savedExportUuid: string;
|
||||
savedExportAlias: string;
|
||||
}
|
||||
|
||||
const props = defineProps<SavedExportButtonsConfig>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<generate-button
|
||||
:saved-export-uuid="props.savedExportUuid"
|
||||
></generate-button>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
@@ -0,0 +1,186 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
trans,
|
||||
SAVED_EXPORT_EXECUTE,
|
||||
EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING_SHORT,
|
||||
EXPORT_GENERATION_TOO_MANY_RETRIES,
|
||||
EXPORT_GENERATION_ERROR_WHILE_GENERATING_EXPORT,
|
||||
EXPORT_GENERATION_EXPORT_READY,
|
||||
} from "translator";
|
||||
import {
|
||||
fetchExportGenerationStatus,
|
||||
generateFromSavedExport,
|
||||
} from "ChillMainAssets/lib/api/export";
|
||||
import { computed, ref } from "vue";
|
||||
import { ExportGeneration } from "ChillMainAssets/types";
|
||||
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
|
||||
import DownloadButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DownloadButton.vue";
|
||||
import { useToast } from "vue-toast-notification";
|
||||
import "vue-toast-notification/dist/theme-sugar.css";
|
||||
|
||||
interface SavedExportButtonGenerateConfig {
|
||||
savedExportUuid: string;
|
||||
}
|
||||
|
||||
const props = defineProps<SavedExportButtonGenerateConfig>();
|
||||
const emits = defineEmits<{
|
||||
(e: "generate");
|
||||
}>();
|
||||
|
||||
const toast = useToast();
|
||||
const exportGeneration = ref<ExportGeneration | null>(null);
|
||||
|
||||
const status = computed<StoredObjectStatus | "inactive">(
|
||||
() => exportGeneration.value?.status ?? "inactive",
|
||||
);
|
||||
const storedObject = computed<null | StoredObject>(() => {
|
||||
if (exportGeneration.value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return exportGeneration.value?.storedObject;
|
||||
});
|
||||
|
||||
const isInactive = computed<boolean>(() => status.value === "inactive");
|
||||
const isPending = computed<boolean>(() => status.value === "pending");
|
||||
const isFetching = computed<boolean>(
|
||||
() => tryiesForReady.value < maxTryiesForReady,
|
||||
);
|
||||
const isReady = computed<boolean>(() => status.value === "ready");
|
||||
const isFailure = computed<boolean>(() => status.value === "failure");
|
||||
const filename = computed<string>(() => {
|
||||
if (null === exportGeneration.value) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `${exportGeneration.value?.storedObject.title}-${exportGeneration.value?.createdAt?.datetime8601}`;
|
||||
});
|
||||
const externalDownloadLink = computed<string>(
|
||||
() => `/fr/main/export-generation/${exportGeneration.value?.id}/wait`,
|
||||
);
|
||||
const classes = computed<Record<string, boolean>>(() => {
|
||||
return {};
|
||||
});
|
||||
const buttonClasses = computed<Record<string, boolean>>(() => {
|
||||
return { btn: true, "btn-outline-primary": true };
|
||||
});
|
||||
|
||||
/**
|
||||
* counter for the number of times that we check for a new status
|
||||
*/
|
||||
let tryiesForReady = ref<number>(0);
|
||||
|
||||
/**
|
||||
* how many times we may check for a new status, once loaded
|
||||
*/
|
||||
const maxTryiesForReady = 120;
|
||||
|
||||
const checkForReady = function (): void {
|
||||
if (
|
||||
"ready" === status.value ||
|
||||
"empty" === status.value ||
|
||||
"failure" === status.value ||
|
||||
// stop reloading if the page stays opened for a long time
|
||||
tryiesForReady.value > maxTryiesForReady
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
tryiesForReady.value = tryiesForReady.value + 1;
|
||||
setTimeout(
|
||||
onObjectNewStatusCallback,
|
||||
tryiesForReady.value < 10 ? 1500 : 5000,
|
||||
);
|
||||
};
|
||||
|
||||
const onExportGenerationSuccess = function (): void {
|
||||
toast.success(trans(EXPORT_GENERATION_EXPORT_READY));
|
||||
};
|
||||
|
||||
const onObjectNewStatusCallback = async function (): Promise<void> {
|
||||
if (null === exportGeneration.value) {
|
||||
checkForReady();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const newExportGeneration = await fetchExportGenerationStatus(
|
||||
exportGeneration.value?.id,
|
||||
);
|
||||
|
||||
if (newExportGeneration.status !== exportGeneration.value.status) {
|
||||
if (newExportGeneration.status === "ready") {
|
||||
onExportGenerationSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
exportGeneration.value = newExportGeneration;
|
||||
|
||||
if (isPending.value) {
|
||||
checkForReady();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const onClickGenerate = async (): Promise<void> => {
|
||||
emits("generate");
|
||||
exportGeneration.value = await generateFromSavedExport(
|
||||
props.savedExportUuid,
|
||||
);
|
||||
|
||||
onObjectNewStatusCallback();
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
v-if="isInactive"
|
||||
:class="buttonClasses"
|
||||
type="button"
|
||||
@click="onClickGenerate"
|
||||
>
|
||||
<i class="fa fa-cog"></i> {{ trans(SAVED_EXPORT_EXECUTE) }}
|
||||
</button>
|
||||
<template v-if="isPending && isFetching">
|
||||
<span class="btn">
|
||||
<i class="fa fa-cog fa-spin fa-fw"></i>
|
||||
<span class="pending-message">{{
|
||||
trans(EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING_SHORT)
|
||||
}}</span>
|
||||
<a :href="externalDownloadLink" class="externalDownloadLink">
|
||||
<i class="bi bi-box-arrow-up-right"></i>
|
||||
</a>
|
||||
</span>
|
||||
</template>
|
||||
<div v-if="isPending && !isFetching" :class="buttonClasses">
|
||||
<span>{{ trans(EXPORT_GENERATION_TOO_MANY_RETRIES) }}</span>
|
||||
</div>
|
||||
<download-button
|
||||
v-else-if="isReady && storedObject?.currentVersion !== null"
|
||||
:classes="buttonClasses"
|
||||
:stored-object="storedObject"
|
||||
:at-version="storedObject?.currentVersion"
|
||||
:filename="filename"
|
||||
></download-button>
|
||||
<div v-else-if="isFailure" :class="classes">
|
||||
<span class="btn">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
<span class="pending-message">{{
|
||||
trans(EXPORT_GENERATION_ERROR_WHILE_GENERATING_EXPORT)
|
||||
}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pending-message {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.externalDownloadLink {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,13 @@
|
||||
import { createApp } from "vue";
|
||||
|
||||
import App from "./App.vue";
|
||||
|
||||
const buttons = document.querySelectorAll<HTMLDivElement>(
|
||||
"[data-generate-export-button]",
|
||||
);
|
||||
|
||||
buttons.forEach((button) => {
|
||||
const savedExportUuid = button.dataset.savedExportUuid as string;
|
||||
|
||||
createApp(App, { savedExportUuid, savedExportAlias: "" }).mount(button);
|
||||
});
|
@@ -446,11 +446,31 @@ Toutes les classes btn-* de bootstrap sont fonctionnelles
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<h1>Badges</h1>
|
||||
<h1>Entity badges</h1>
|
||||
|
||||
<span class="badge-accompanying-work-type-simple">Action d'accompagnement</span>
|
||||
<span class="badge-activity-type-simple">Type d'échange</span>
|
||||
<span class="badge-calendar-simple">Rendez-vous</span>
|
||||
</div>
|
||||
|
||||
|
||||
<h1>Badges</h1>
|
||||
|
||||
<p>
|
||||
<span class="badge bg-primary">Primary</span>
|
||||
<span class="badge bg-secondary">Secondary</span>
|
||||
<span class="badge bg-success">Success</span>
|
||||
<span class="badge bg-danger">Danger</span>
|
||||
<span class="badge bg-warning">Warning</span>
|
||||
<span class="badge bg-info">Info</span>
|
||||
<span class="badge bg-light">Light</span>
|
||||
<span class="badge bg-dark">Dark</span>
|
||||
<span class="badge bg-chill-blue">chill-blue</span>
|
||||
<span class="badge bg-chill-green">chill-green</span>
|
||||
<span class="badge bg-chill-yellow">chill-yellow</span>
|
||||
<span class="badge bg-chill-orange">chill-orange</span>
|
||||
<span class="badge bg-chill-red">chill-red</span>
|
||||
<span class="badge bg-chill-beige">chill-beige</span>
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
|
@@ -1,12 +1,14 @@
|
||||
<ul class="nav nav-pills justify-content-center">
|
||||
<li class="nav-item">
|
||||
<a href="{{ chill_path_forward_return_path('chill_main_export_index') }}" class="nav-link {% if current == 'common' %}active{% endif %}">
|
||||
{{ 'Exports list'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% if is_granted('CHILL_MAIN_COMPOSE_EXPORT') %}
|
||||
<li class="nav-item">
|
||||
<a href="{{ chill_path_forward_return_path('chill_main_export_index') }}" class="nav-link {% if current == 'common' %}active{% endif %}">
|
||||
{{ 'Exports list'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
<a href="{{ chill_path_forward_return_path('chill_main_export_saved_list_my') }}" class="nav-link {% if current == 'my' %}active{% endif %}">
|
||||
{{ 'saved_export.My saved exports'|trans }}
|
||||
{{ 'saved_export.Saved exports'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{#
|
||||
* Copyright (C) 2014-2015, Champs Libres Cooperative SCRLFS,
|
||||
* Copyright (C) 2014-2015, Champs Libres Cooperative SCRLFS,
|
||||
<info@champs-libres.coop> / <http://www.champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -20,6 +20,49 @@
|
||||
|
||||
{% block title %}{{ 'Exports list'|trans }}{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
<style lang="css">
|
||||
.export-title {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% macro render_export_card(export, export_alias, generations) %}
|
||||
<div class="col">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">{{ export.title|trans }}</h2>
|
||||
<p class="card-text my-3">{{ export.description|trans }}</p>
|
||||
</div>
|
||||
{% if generations|length > 0 %}
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for generation in generations %}
|
||||
<li class="list-group-item">
|
||||
<a href="{{ chill_path_add_return_path('chill_main_export-generation_wait', {'id': generation.id}) }}">{{ generation.createdAt|format_datetime('short', 'short') }}</a>
|
||||
{% if generation.status == 'pending' %}
|
||||
<span class="badge bg-info">{{ 'export.generation.Export generation is pending_short'|trans }}</span>
|
||||
{% elseif generation.status == 'failure' %}
|
||||
<span class="badge bg-warning">{{ 'export.generation.Error_short'|trans }}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<div class="card-footer">
|
||||
<ul class="record_actions slim">
|
||||
<li>
|
||||
<a class="btn btn-submit" href="{{ path('chill_main_export_new', { 'alias': export_alias } ) }}">
|
||||
{{ 'Create an export'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@@ -28,45 +71,23 @@
|
||||
<div class="col-md-10 exports-list">
|
||||
|
||||
<div class="container mt-4">
|
||||
|
||||
|
||||
{% for group, exports in grouped_exports %}{% if group != '_' %}
|
||||
<h2 class="display-6">{{ group|trans }}</h2>
|
||||
<div class="row flex-bloc">
|
||||
<h1 class="display-6 export-title">{{ group|trans }}</h1>
|
||||
<div class="row row-cols-1 row-cols-md-3 g-2">
|
||||
{% for export_alias, export in exports %}
|
||||
<div class="item-bloc">
|
||||
<div class="item-row card-body">
|
||||
<h2 class="card-title">{{ export.title|trans }}</h2>
|
||||
<p class="card-text my-3">{{ export.description|trans }}</p>
|
||||
<p>
|
||||
<a class="btn btn-action" href="{{ path('chill_main_export_new', { 'alias': export_alias } ) }}">
|
||||
{{ 'Create an export'|trans }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ _self.render_export_card(export, export_alias, last_executions[export_alias]) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}{% endfor %}
|
||||
|
||||
|
||||
{% if grouped_exports|keys|length > 1 and grouped_exports['_']|length > 0 %}
|
||||
<h2 class="display-6">{{ 'Ungrouped exports'|trans }}</h2>
|
||||
<h2 class="display-6 export-title">{{ 'Ungrouped exports'|trans }}</h2>
|
||||
{% endif %}
|
||||
|
||||
<div class="row flex-bloc">
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-3 g-2">
|
||||
{% for export_alias,export in grouped_exports['_'] %}
|
||||
|
||||
<div class="item-bloc">
|
||||
<div class="item-row card-body">
|
||||
<h2 class="card-title">{{ export.title|trans }}</h2>
|
||||
<p class="card-text my-3">{{ export.description|trans }}</p>
|
||||
<p>
|
||||
<a class="btn btn-action" href="{{ path('chill_main_export_new', { 'alias': export_alias } ) }}">
|
||||
{{ 'Create an export'|trans }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ _self.render_export_card(export, export_alias, last_executions[export_alias]) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -40,15 +40,15 @@
|
||||
|
||||
<h3 class="m-3">{{ 'Center'|trans }}</h3>
|
||||
|
||||
{{ form_widget(form.centers.center) }}
|
||||
{{ form_widget(form.centers.centers) }}
|
||||
|
||||
<div class="mb-3 mt-3">
|
||||
<input id="toggle-check-all" class="btn btn-misc" type= "button" onclick='uncheckAll(this)' value="{{ 'uncheck all centers'|trans|e('html_attr') }}"/>
|
||||
</div>
|
||||
|
||||
{% if form.centers.regroupment is defined %}
|
||||
{% if form.centers.regroupments is defined %}
|
||||
<h3 class="m-3">{{ 'Pick aggregated centers'|trans }}</h3>
|
||||
{{ form_widget(form.centers.regroupment) }}
|
||||
{{ form_widget(form.centers.regroupments) }}
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
|
@@ -0,0 +1,61 @@
|
||||
{% extends '@ChillMain/layout.html.twig' %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('page_download_exports') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('page_download_exports') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block title exportGeneration.linkedToSavedExport ? exportGeneration.savedExport.title : ('Download export'|trans) %}
|
||||
|
||||
{% 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') }}"
|
||||
data-export-title="{{ export.title|trans }}"
|
||||
></div>
|
||||
|
||||
<ul class="sticky-form-buttons record_actions">
|
||||
<li class="cancel">
|
||||
<a href="{{ chill_return_path_or('chill_main_export_saved_list_my') }}" class="btn btn-cancel">
|
||||
{{ 'export.generation.Come back later'|trans|chill_return_path_label }}
|
||||
</a>
|
||||
</li>
|
||||
{% 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>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li class="dropdown-item">
|
||||
<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">
|
||||
<form method="POST" action="{{ path('chill_main_export_saved_options_edit', {'savedExport': exportGeneration.savedExport.id, 'exportGeneration': exportGeneration.id }) }}">
|
||||
<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 %}
|
||||
</ul>
|
||||
{% endblock content %}
|
@@ -2,6 +2,16 @@
|
||||
|
||||
{% block title %}{{ 'saved_export.Edit'|trans }}{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_pickentity_type') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-10">
|
||||
<h1>{{ block('title') }}</h1>
|
||||
@@ -10,9 +20,13 @@
|
||||
{{ form_row(form.title) }}
|
||||
{{ form_row(form.description) }}
|
||||
|
||||
{% if form.share is defined %}
|
||||
{{ form_row(form.share) }}
|
||||
{% endif %}
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
<a href="{{ chill_return_path_or('chill_main_homepage') }}" class="btn btn-cancel">{{ 'Cancel'|trans }}</a>
|
||||
<a href="{{ chill_return_path_or('chill_main_export_saved_list_my') }}" class="btn btn-cancel">{{ 'Cancel'|trans }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<button type="submit" class="btn btn-save">{{ 'Save'|trans }}</button>
|
||||
@@ -20,4 +34,4 @@
|
||||
</ul>
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
@@ -1,6 +1,88 @@
|
||||
{% extends "@ChillMain/layout.html.twig" %}
|
||||
|
||||
{% block title %}{{ 'saved_export.My saved exports'|trans }}{% endblock %}
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_saved_export_button') }}
|
||||
<style lang="css">
|
||||
.export-title {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_saved_export_button') }}
|
||||
{% endblock %}
|
||||
|
||||
{% 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">
|
||||
<div class="card-header">
|
||||
{{ export.title|trans }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">{{ saved.title }}</h2>
|
||||
{% if app.user is same as saved.user %}
|
||||
<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 saved.isShared() %}<span class="badge bg-info">{{ 'saved_export.Shared with others'|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>
|
||||
{% if generations|length > 0 %}
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for generation in generations %}
|
||||
<li class="list-group-item">
|
||||
<a href="{{ chill_path_add_return_path('chill_main_export-generation_wait', {'id': generation.id}) }}">{{ generation.createdAt|format_datetime('short', 'short') }}</a>
|
||||
{% if generation.status == 'pending' %}
|
||||
<span class="badge bg-info">{{ 'export.generation.Export generation is pending_short'|trans }}</span>
|
||||
{% elseif generation.status == 'failure' %}
|
||||
<span class="badge bg-warning">{{ 'export.generation.Error_short'|trans }}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<div class="card-footer">
|
||||
<ul class="record_actions slim">
|
||||
<li>
|
||||
<div class="" data-generate-export-button data-saved-export-uuid="{{ saved.id|escape('html_attr') }}"></div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{{ 'Actions'|trans }}
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% if is_granted('CHILL_MAIN_EXPORT_SAVED_EDIT', saved) %}
|
||||
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_edit', {'id': saved.id }) }}" class="dropdown-item"><i class="fa fa-pencil"></i> {{ 'saved_export.update_title_and_description'|trans }}</a></li>
|
||||
{% 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 %}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-md-10 exports-list">
|
||||
@@ -8,6 +90,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>
|
||||
@@ -15,71 +98,23 @@
|
||||
|
||||
{% for group, saveds in grouped_exports %}
|
||||
{% if group != '_' %}
|
||||
<h2 class="display-6">{{ group }}</h2>
|
||||
<div class="row flex-bloc">
|
||||
<h1 class="display-6 export-title">{{ group }}</h1>
|
||||
<div class="row row-cols-1 row-cols-md-3 g-2">
|
||||
{% for s in saveds %}
|
||||
<div class="item-bloc">
|
||||
<div class="item-row card-body">
|
||||
<p class="card-subtitle"><strong>{{ s.export.title|trans }}</strong></p>
|
||||
<h2 class="card-title">{{ s.saved.title }}</h2>
|
||||
|
||||
<div class="createdBy">{{ 'saved_export.Created on %date%'|trans({'%date%': s.saved.createdAt|format_datetime('long', 'short')}) }}</div>
|
||||
|
||||
<div class="card-text my-3">
|
||||
{{ s.saved.description|chill_markdown_to_html }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{{ 'Actions'|trans }}
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_edit', {'id': s.saved.id }) }}" class="dropdown-item"><i class="fa fa-pencil"></i> {{ 'saved_export.update_title_and_description'|trans }}</a></li>
|
||||
<li><a href="{{ chill_path_add_return_path('chill_main_export_new', {'alias': s.saved.exportAlias,'from_saved': s.saved.id }) }}" class="dropdown-item"><i class="fa fa-pencil"></i> {{ 'saved_export.update_filters_aggregators_and_execute'|trans }}</a></li>
|
||||
<li><a href="{{ path('chill_main_export_generate_from_saved', { id: s.saved.id }) }}" class="dropdown-item"><i class="fa fa-cog"></i> {{ 'saved_export.execute'|trans }}</a></li>
|
||||
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_delete', {'id': s.saved.id }) }}" class="dropdown-item"><i class="fa fa-trash"></i> {{ 'Delete'|trans }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ _self.render_export_card(s.saved, s.export, s.saved.exportAlias, last_executions[s.saved.id.toString()]) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if grouped_exports|keys|length > 1 and grouped_exports['_']|default([])|length > 0 %}
|
||||
<h2 class="display-6">{{ 'Ungrouped exports'|trans }}</h2>
|
||||
<h2 class="display-6 export-title">{{ 'Ungrouped exports'|trans }}</h2>
|
||||
{% endif %}
|
||||
|
||||
<div class="row flex-bloc">
|
||||
{% for saveds in grouped_exports['_']|default([]) %}
|
||||
{% for s in saveds %}
|
||||
<div class="item-bloc">
|
||||
<div class="item-row card-body">
|
||||
<p class="card-subtitle"><strong>{{ s.export.title|trans }}</strong></p>
|
||||
<h2 class="card-title">{{ s.saved.title }}</h2>
|
||||
|
||||
<div class="createdBy">{{ 'saved_export.Created on %date%'|trans({'%date%': s.saved.createdAt|format_datetime('long', 'short')}) }}</div>
|
||||
|
||||
<div class="card-text my-3">
|
||||
{{ s.saved.description|chill_markdown_to_html }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ul class="record_actions">
|
||||
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_delete', {'id': s.saved.id }) }}" class="btn btn-delete"></a></li>
|
||||
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_edit', {'id': s.saved.id }) }}" class="btn btn-edit"></a></li>
|
||||
<li><a href="{{ path('chill_main_export_generate_from_saved', { id: s.saved.id }) }}" class="btn btn-action"><i class="fa fa-cog"></i></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ _self.render_export_card(s.saved, s.export, s.saved.exportAlias, last_executions[s.saved.id.toString()]) }}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
@@ -2,14 +2,35 @@
|
||||
|
||||
{% block title %}{{ 'saved_export.New'|trans }}{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_pickentity_type') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-10">
|
||||
<h1>{{ block('title') }}</h1>
|
||||
|
||||
{{ 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 %}
|
||||
{{ form_row(form.share) }}
|
||||
{% endif %}
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
<a href="{{ chill_return_path_or('chill_main_homepage') }}" class="btn btn-cancel">{{ 'Cancel'|trans }}</a>
|
||||
@@ -20,4 +41,4 @@
|
||||
</ul>
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
@@ -10,19 +10,19 @@
|
||||
|
||||
{% for flashMessage in app.session.flashbag.get('success') %}
|
||||
<div class="col-8 alert alert-success flash_message">
|
||||
<span>{{ flashMessage|raw }}</span>
|
||||
<span>{{ flashMessage|trans }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for flashMessage in app.session.flashbag.get('error') %}
|
||||
<div class="col-8 alert alert-danger flash_message">
|
||||
<span>{{ flashMessage|raw }}</span>
|
||||
<span>{{ flashMessage|trans }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for flashMessage in app.session.flashbag.get('notice') %}
|
||||
<div class="col-8 alert alert-warning flash_message">
|
||||
<span>{{ flashMessage|raw }}</span>
|
||||
<span>{{ flashMessage|trans }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
|
@@ -49,9 +49,9 @@ class SectionMenuBuilder implements LocalMenuBuilderInterface
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->authorizationChecker->isGranted(ChillExportVoter::EXPORT)) {
|
||||
if ($this->authorizationChecker->isGranted(ChillExportVoter::GENERATE_SAVED_EXPORT)) {
|
||||
$menu->addChild($this->translator->trans('Export Menu'), [
|
||||
'route' => 'chill_main_export_index',
|
||||
'route' => 'chill_main_export_saved_list_my',
|
||||
])
|
||||
->setExtras([
|
||||
'icons' => ['upload'],
|
||||
|
@@ -11,12 +11,21 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Security\Authorization;
|
||||
|
||||
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
|
||||
class ChillExportVoter extends Voter
|
||||
class ChillExportVoter extends Voter implements ProvideRoleHierarchyInterface
|
||||
{
|
||||
final public const EXPORT = 'chill_export';
|
||||
/**
|
||||
* Role which give access to the creation of new export from the export itself.
|
||||
*/
|
||||
final public const COMPOSE_EXPORT = 'CHILL_MAIN_COMPOSE_EXPORT';
|
||||
|
||||
/**
|
||||
* Role which give access to the execution and edition to the saved exports, but not for creating new ones.
|
||||
*/
|
||||
final public const GENERATE_SAVED_EXPORT = 'CHILL_MAIN_GENERATE_SAVED_EXPORT';
|
||||
|
||||
private readonly VoterHelperInterface $helper;
|
||||
|
||||
@@ -24,7 +33,7 @@ class ChillExportVoter extends Voter
|
||||
{
|
||||
$this->helper = $voterHelperFactory
|
||||
->generate(self::class)
|
||||
->addCheckFor(null, [self::EXPORT])
|
||||
->addCheckFor(null, [self::COMPOSE_EXPORT, self::GENERATE_SAVED_EXPORT])
|
||||
->build();
|
||||
}
|
||||
|
||||
@@ -37,4 +46,21 @@ class ChillExportVoter extends Voter
|
||||
{
|
||||
return $this->helper->voteOnAttribute($attribute, $subject, $token);
|
||||
}
|
||||
|
||||
public function getRolesWithHierarchy(): array
|
||||
{
|
||||
return ['export.role.export_role' => [
|
||||
self::COMPOSE_EXPORT, self::GENERATE_SAVED_EXPORT,
|
||||
]];
|
||||
}
|
||||
|
||||
public function getRoles(): array
|
||||
{
|
||||
return [self::COMPOSE_EXPORT, self::GENERATE_SAVED_EXPORT];
|
||||
}
|
||||
|
||||
public function getRolesWithoutScope(): array
|
||||
{
|
||||
return $this->getRoles();
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,32 @@
|
||||
<?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\Security\Authorization;
|
||||
|
||||
use Chill\MainBundle\Entity\ExportGeneration;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
|
||||
class ExportGenerationVoter extends Voter
|
||||
{
|
||||
public const VIEW = 'view';
|
||||
|
||||
protected function supports(string $attribute, $subject)
|
||||
{
|
||||
return self::VIEW === $attribute && $subject instanceof ExportGeneration;
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token)
|
||||
{
|
||||
/* @var ExportGeneration $subject */
|
||||
return $token->getUser()->getUserIdentifier() === $subject->getCreatedBy()->getUserIdentifier();
|
||||
}
|
||||
}
|
@@ -12,23 +12,37 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Security\Authorization;
|
||||
|
||||
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;
|
||||
|
||||
class SavedExportVoter extends Voter
|
||||
final class SavedExportVoter extends Voter
|
||||
{
|
||||
final public const DELETE = 'CHLL_MAIN_EXPORT_SAVED_DELETE';
|
||||
final public const DELETE = 'CHILL_MAIN_EXPORT_SAVED_DELETE';
|
||||
|
||||
final public const EDIT = 'CHLL_MAIN_EXPORT_SAVED_EDIT';
|
||||
final public const EDIT = 'CHILL_MAIN_EXPORT_SAVED_EDIT';
|
||||
|
||||
final public const GENERATE = 'CHLL_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';
|
||||
|
||||
private const ALL = [
|
||||
self::DELETE,
|
||||
self::EDIT,
|
||||
self::GENERATE,
|
||||
self::SHARE,
|
||||
self::DUPLICATE,
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly ExportManager $exportManager,
|
||||
private readonly AccessDecisionManagerInterface $accessDecisionManager,
|
||||
) {}
|
||||
|
||||
protected function supports($attribute, $subject): bool
|
||||
{
|
||||
return $subject instanceof SavedExport && \in_array($attribute, self::ALL, true);
|
||||
@@ -36,9 +50,30 @@ class SavedExportVoter extends Voter
|
||||
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||
{
|
||||
/* @var SavedExport $subject */
|
||||
$user = $token->getUser();
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return match ($attribute) {
|
||||
self::DELETE, self::EDIT, self::GENERATE => $subject->getUser() === $token->getUser(),
|
||||
self::DELETE, self::EDIT => $subject->getUser() === $token->getUser(),
|
||||
self::SHARE => $subject->getUser() === $token->getUser() && $this->accessDecisionManager->decide($token, [ChillExportVoter::COMPOSE_EXPORT]),
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
private function canUserGenerate(User $user, SavedExport $savedExport): bool
|
||||
{
|
||||
if (!($savedExport->getUser() === $user || $savedExport->isSharedWithUser($user))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$export = $this->exportManager->getExport($savedExport->getExportAlias());
|
||||
|
||||
return $this->exportManager->isGrantedForElement($export);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,43 @@
|
||||
<?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\Security\Authorization\StoredObject;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
|
||||
use Chill\MainBundle\Repository\ExportGenerationRepository;
|
||||
use Chill\MainBundle\Security\Authorization\ExportGenerationVoter;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
final readonly class ExportGenerationStoredObjectVoter implements StoredObjectVoterInterface
|
||||
{
|
||||
public function __construct(private ExportGenerationRepository $repository, private Security $security) {}
|
||||
|
||||
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool
|
||||
{
|
||||
return null !== $this->repository->findAssociatedEntityToStoredObject($subject);
|
||||
}
|
||||
|
||||
public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool
|
||||
{
|
||||
if (StoredObjectRoleEnum::EDIT === $attribute) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (null === $generation = $this->repository->findAssociatedEntityToStoredObject($subject)) {
|
||||
throw new \UnexpectedValueException('generation not found');
|
||||
}
|
||||
|
||||
return $this->security->isGranted(ExportGenerationVoter::VIEW, $generation);
|
||||
}
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\MainBundle\Service\Regroupement;
|
||||
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\Regroupment;
|
||||
|
||||
class CenterRegroupementResolver
|
||||
{
|
||||
/**
|
||||
* Resolves and returns a unique list of centers by merging those from the provided
|
||||
* groups and the additional centers while eliminating duplicates.
|
||||
*
|
||||
* @param list<Regroupment> $groups
|
||||
* @param list<Center> $centers
|
||||
*
|
||||
* @return list<Center>
|
||||
*/
|
||||
public function resolveCenters(array $groups, array $centers = []): array
|
||||
{
|
||||
$centersByHash = [];
|
||||
|
||||
foreach ($groups as $group) {
|
||||
foreach ($group->getCenters() as $center) {
|
||||
$centersByHash[spl_object_hash($center)] = $center;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($centers as $center) {
|
||||
$centersByHash[spl_object_hash($center)] = $center;
|
||||
}
|
||||
|
||||
return array_values($centersByHash);
|
||||
}
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
<?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\Service\Regroupement;
|
||||
|
||||
use Chill\MainBundle\Entity\Regroupment;
|
||||
|
||||
/**
|
||||
* Class RegroupementFiltering.
|
||||
*
|
||||
* Provides methods to filter and manage groups based on specific criteria.
|
||||
*/
|
||||
class RegroupementFiltering
|
||||
{
|
||||
/**
|
||||
* Filters the provided groups and returns only those that contain at least one of the specified centers.
|
||||
*
|
||||
* @param array $groups an array of groups to filter
|
||||
* @param array $centers an array of centers to check against the groups
|
||||
*
|
||||
* @return array an array of filtered groups containing at least one of the specified centers
|
||||
*/
|
||||
public function filterContainsAtLeastOneCenter(array $groups, array $centers): array
|
||||
{
|
||||
return array_values(
|
||||
array_filter($groups, static fn (Regroupment $group) => $group->containsAtLeastOneCenter($centers)),
|
||||
);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user