mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Add auto-generated export descriptions and helper service
Introduce `ExportDescriptionHelper` to dynamically generate default descriptions for exports based on current settings. Update controllers, templates, and test cases to support and display the new auto-generated descriptions. This also adds a warning in the UI to prompt users to adjust these descriptions as needed.
This commit is contained in:
parent
be448c650e
commit
3a016aa12a
@ -14,6 +14,7 @@ namespace Chill\MainBundle\Controller;
|
|||||||
use Chill\MainBundle\Entity\ExportGeneration;
|
use Chill\MainBundle\Entity\ExportGeneration;
|
||||||
use Chill\MainBundle\Entity\SavedExport;
|
use Chill\MainBundle\Entity\SavedExport;
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Export\ExportDescriptionHelper;
|
||||||
use Chill\MainBundle\Export\ExportManager;
|
use Chill\MainBundle\Export\ExportManager;
|
||||||
use Chill\MainBundle\Form\SavedExportType;
|
use Chill\MainBundle\Form\SavedExportType;
|
||||||
use Chill\MainBundle\Security\Authorization\ExportGenerationVoter;
|
use Chill\MainBundle\Security\Authorization\ExportGenerationVoter;
|
||||||
@ -44,6 +45,7 @@ final readonly class SavedExportController
|
|||||||
private Security $security,
|
private Security $security,
|
||||||
private TranslatorInterface $translator,
|
private TranslatorInterface $translator,
|
||||||
private UrlGeneratorInterface $urlGenerator,
|
private UrlGeneratorInterface $urlGenerator,
|
||||||
|
private ExportDescriptionHelper $exportDescriptionHelper,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Route(path: '/{_locale}/exports/saved/{id}/delete', name: 'chill_main_export_saved_delete')]
|
#[Route(path: '/{_locale}/exports/saved/{id}/delete', name: 'chill_main_export_saved_delete')]
|
||||||
@ -107,7 +109,21 @@ final readonly class SavedExportController
|
|||||||
$request->query->has('title') ? $request->query->get('title') : $title
|
$request->query->has('title') ? $request->query->get('title') : $title
|
||||||
);
|
);
|
||||||
|
|
||||||
return $this->handleEdit($savedExport, $request);
|
if ($exportGeneration->isLinkedToSavedExport()) {
|
||||||
|
$savedExport->setDescription($exportGeneration->getSavedExport()->getDescription());
|
||||||
|
} else {
|
||||||
|
$savedExport->setDescription(
|
||||||
|
implode(
|
||||||
|
"\n",
|
||||||
|
array_map(
|
||||||
|
fn (string $item) => '- '.$item."\n",
|
||||||
|
$this->exportDescriptionHelper->describe($savedExport->getExportAlias(), $savedExport->getOptions(), includeExportTitle: false)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->handleEdit($savedExport, $request, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route(path: '/exports/saved/duplicate-from-saved-export/{id}/new', name: 'chill_main_export_saved_duplicate')]
|
#[Route(path: '/exports/saved/duplicate-from-saved-export/{id}/new', name: 'chill_main_export_saved_duplicate')]
|
||||||
@ -138,7 +154,7 @@ final readonly class SavedExportController
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function handleEdit(SavedExport $savedExport, Request $request): Response
|
private function handleEdit(SavedExport $savedExport, Request $request, bool $showWarningAutoGeneratedDescription = false): Response
|
||||||
{
|
{
|
||||||
$form = $this->formFactory->create(SavedExportType::class, $savedExport);
|
$form = $this->formFactory->create(SavedExportType::class, $savedExport);
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
@ -161,6 +177,7 @@ final readonly class SavedExportController
|
|||||||
'@ChillMain/SavedExport/new.html.twig',
|
'@ChillMain/SavedExport/new.html.twig',
|
||||||
[
|
[
|
||||||
'form' => $form->createView(),
|
'form' => $form->createView(),
|
||||||
|
'showWarningAutoGeneratedDescription' => $showWarningAutoGeneratedDescription,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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] ?? []);
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,13 @@
|
|||||||
|
|
||||||
{{ form_start(form) }}
|
{{ form_start(form) }}
|
||||||
{{ form_row(form.title) }}
|
{{ form_row(form.title) }}
|
||||||
|
|
||||||
|
{% if showWarningAutoGeneratedDescription|default(false) %}
|
||||||
|
<div class="alert alert-info" role="alert">
|
||||||
|
{{ 'saved_export.Alert auto generated description'|trans }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{{ form_row(form.description) }}
|
{{ form_row(form.description) }}
|
||||||
|
|
||||||
{% if form.share is defined %}
|
{% if form.share is defined %}
|
||||||
|
@ -0,0 +1,174 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Tests\Export;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Export\AggregatorInterface;
|
||||||
|
use Chill\MainBundle\Export\ExportConfigNormalizer;
|
||||||
|
use Chill\MainBundle\Export\ExportConfigProcessor;
|
||||||
|
use Chill\MainBundle\Export\ExportDescriptionHelper;
|
||||||
|
use Chill\MainBundle\Export\ExportGenerationContext;
|
||||||
|
use Chill\MainBundle\Export\ExportInterface;
|
||||||
|
use Chill\MainBundle\Export\ExportManager;
|
||||||
|
use Chill\MainBundle\Export\FilterInterface;
|
||||||
|
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
use Symfony\Contracts\Translation\TranslatableInterface;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class ExportDescriptionHelperTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
private const JSON_HAPPY_SCENARIO = <<<'JSON'
|
||||||
|
{
|
||||||
|
"export": {
|
||||||
|
"form": [],
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
"centers": {
|
||||||
|
"centers": [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"regroupments": []
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"my_filter_string": {
|
||||||
|
"form": {
|
||||||
|
"accepted_socialissues": [
|
||||||
|
2,
|
||||||
|
3
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"enabled": true,
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
"my_filter_array": {
|
||||||
|
"form": {
|
||||||
|
"misc": true
|
||||||
|
},
|
||||||
|
"enabled": true,
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
"my_filter_translatable": {
|
||||||
|
"form": {
|
||||||
|
"misc": true
|
||||||
|
},
|
||||||
|
"enabled": true,
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"form": {
|
||||||
|
"format": "xlsx",
|
||||||
|
"activity_user_aggregator": {
|
||||||
|
"order": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"version": 1
|
||||||
|
},
|
||||||
|
"aggregators": {
|
||||||
|
"my_aggregator": {
|
||||||
|
"form": {"key": 1},
|
||||||
|
"enabled": true,
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pick_formatter": "spreadsheet"
|
||||||
|
}
|
||||||
|
JSON;
|
||||||
|
|
||||||
|
public function testDescribeHappyScenario(): void
|
||||||
|
{
|
||||||
|
$options = json_decode(self::JSON_HAPPY_SCENARIO, true);
|
||||||
|
|
||||||
|
$security = $this->prophesize(Security::class);
|
||||||
|
$security->getUser()->willReturn($user = new User());
|
||||||
|
|
||||||
|
$exportConfigNormalizer = $this->prophesize(ExportConfigNormalizer::class);
|
||||||
|
$exportConfigNormalizer->denormalizeConfig('my_export', Argument::type('array'))->willReturn($options);
|
||||||
|
|
||||||
|
$export = $this->prophesize(ExportInterface::class);
|
||||||
|
$export->getTitle()->willReturn('Title');
|
||||||
|
|
||||||
|
$myFilterString = $this->prophesize(FilterInterface::class);
|
||||||
|
$myFilterString->describeAction(Argument::type('array'), Argument::type(ExportGenerationContext::class))->willReturn($string0 = 'This is a filter description');
|
||||||
|
$myFilterArray = $this->prophesize(FilterInterface::class);
|
||||||
|
$myFilterArray->describeAction(Argument::type('array'), Argument::type(ExportGenerationContext::class))->willReturn([$string1 = 'This is a filter with %argument%', $arg1 = ['%argument%' => 'zero']]);
|
||||||
|
$myFilterTranslatable = $this->prophesize(FilterInterface::class);
|
||||||
|
$myFilterTranslatable->describeAction(Argument::type('array'), Argument::type(ExportGenerationContext::class))
|
||||||
|
->willReturn(new class () implements TranslatableInterface {
|
||||||
|
public function trans(TranslatorInterface $translator, ?string $locale = null): string
|
||||||
|
{
|
||||||
|
return 'translatable';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$myAggregator = $this->prophesize(AggregatorInterface::class);
|
||||||
|
$myAggregator->getTitle()->willReturn('Some aggregator');
|
||||||
|
|
||||||
|
$token = new UsernamePasswordToken($user, 'main', ['ROLE_USER']);
|
||||||
|
$tokenStorage = new TokenStorage();
|
||||||
|
$tokenStorage->setToken($token);
|
||||||
|
|
||||||
|
$exportManager = new ExportManager(
|
||||||
|
new NullLogger(),
|
||||||
|
$security->reveal(),
|
||||||
|
$this->prophesize(AuthorizationHelperInterface::class)->reveal(),
|
||||||
|
$tokenStorage,
|
||||||
|
['my_export' => $export->reveal()],
|
||||||
|
['my_aggregator' => $myAggregator->reveal()],
|
||||||
|
[
|
||||||
|
'my_filter_string' => $myFilterString->reveal(),
|
||||||
|
'my_filter_array' => $myFilterArray->reveal(),
|
||||||
|
'my_filter_translatable' => $myFilterTranslatable->reveal(),
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
$exportConfigProcessor = new ExportConfigProcessor($exportManager);
|
||||||
|
|
||||||
|
$translator = $this->prophesize(TranslatorInterface::class);
|
||||||
|
$translator->trans('Title')->shouldBeCalled()->willReturn('Title');
|
||||||
|
$translator->trans($string0)->shouldBeCalled()->willReturn($string0);
|
||||||
|
$translator->trans($string1, $arg1)->shouldBeCalled()->willReturn($string1);
|
||||||
|
$translator->trans('Some aggregator')->shouldBeCalled()->willReturn('Some aggregator');
|
||||||
|
|
||||||
|
$exportDescriptionHelper = new ExportDescriptionHelper(
|
||||||
|
$exportManager,
|
||||||
|
$exportConfigNormalizer->reveal(),
|
||||||
|
$exportConfigProcessor,
|
||||||
|
$translator->reveal(),
|
||||||
|
$security->reveal(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$actual = $exportDescriptionHelper->describe('my_export', $options);
|
||||||
|
|
||||||
|
self::assertIsArray($actual);
|
||||||
|
self::assertEquals($actual[0], 'Title');
|
||||||
|
self::assertEquals($actual[1], 'This is a filter description');
|
||||||
|
self::assertEquals($actual[2], 'This is a filter with %argument%');
|
||||||
|
self::assertEquals($actual[3], 'translatable');
|
||||||
|
self::assertEquals($actual[4], 'Some aggregator');
|
||||||
|
}
|
||||||
|
}
|
@ -348,7 +348,6 @@ final class ExportManagerTest extends KernelTestCase
|
|||||||
$logger ?? self::getContainer()->get(LoggerInterface::class),
|
$logger ?? self::getContainer()->get(LoggerInterface::class),
|
||||||
$authorizationChecker ?? self::getContainer()->get('security.authorization_checker'),
|
$authorizationChecker ?? self::getContainer()->get('security.authorization_checker'),
|
||||||
$authorizationHelper ?? self::getContainer()->get('chill.main.security.authorization.helper'),
|
$authorizationHelper ?? self::getContainer()->get('chill.main.security.authorization.helper'),
|
||||||
$tokenStorage,
|
|
||||||
$exports,
|
$exports,
|
||||||
$aggregators,
|
$aggregators,
|
||||||
$filters,
|
$filters,
|
||||||
|
@ -18,6 +18,8 @@ services:
|
|||||||
|
|
||||||
Chill\MainBundle\Export\ExportConfigProcessor: ~
|
Chill\MainBundle\Export\ExportConfigProcessor: ~
|
||||||
|
|
||||||
|
Chill\MainBundle\Export\ExportDescriptionHelper: ~
|
||||||
|
|
||||||
chill.main.export_element_validator:
|
chill.main.export_element_validator:
|
||||||
class: Chill\MainBundle\Validator\Constraints\Export\ExportElementConstraintValidator
|
class: Chill\MainBundle\Validator\Constraints\Export\ExportElementConstraintValidator
|
||||||
tags:
|
tags:
|
||||||
|
@ -807,6 +807,7 @@ saved_export:
|
|||||||
Duplicated: Dupliqué
|
Duplicated: Dupliqué
|
||||||
Options updated successfully: La configuration de l'export a été mise à jour
|
Options updated successfully: La configuration de l'export a été mise à jour
|
||||||
Share: Partage
|
Share: Partage
|
||||||
|
Alert auto generated description: La description ci-dessous a été générée automatiquement, comme si l'export était exécutée immédiatement. Veillez à l'adapter pour tenir compte des paramètres qui peuvent être modifiés (utilisateurs courant, dates glissantes, etc.).
|
||||||
|
|
||||||
absence:
|
absence:
|
||||||
# single letter for absence
|
# single letter for absence
|
||||||
|
Loading…
x
Reference in New Issue
Block a user