[export] sort filters and aggregators by title

This commit is contained in:
2023-10-19 14:04:24 +02:00
parent 4c9ea740c8
commit 5f805626f7
11 changed files with 294 additions and 38 deletions

View File

@@ -11,6 +11,7 @@ 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;
@@ -54,20 +55,17 @@ class ExportManager
*/
private array $formatters = [];
private readonly string|\Stringable|\Symfony\Component\Security\Core\User\UserInterface $user;
public function __construct(
private readonly LoggerInterface $logger,
private readonly AuthorizationCheckerInterface $authorizationChecker,
private readonly AuthorizationHelperInterface $authorizationHelper,
TokenStorageInterface $tokenStorage,
private readonly TokenStorageInterface $tokenStorage,
iterable $exports,
iterable $aggregators,
iterable $filters
// iterable $formatters,
// iterable $exportElementProvider
) {
$this->user = $tokenStorage->getToken()->getUser();
$this->exports = iterator_to_array($exports);
$this->aggregators = iterator_to_array($aggregators);
$this->filters = iterator_to_array($filters);
@@ -91,20 +89,24 @@ class ExportManager
*
* @return FilterInterface[] a \Generator that contains filters. The key is the filter's alias
*/
public function &getFiltersApplyingOn(DirectExportInterface|ExportInterface $export, array $centers = null): iterable
public function getFiltersApplyingOn(DirectExportInterface|ExportInterface $export, array $centers = null): array
{
if ($export instanceof DirectExportInterface) {
return;
return [];
}
$filters = [];
foreach ($this->filters as $alias => $filter) {
if (
\in_array($filter->applyOn(), $export->supportsModifiers(), true)
&& $this->isGrantedForElement($filter, $export, $centers)
) {
yield $alias => $filter;
$filters[$alias] = $filter;
}
}
return $filters;
}
/**
@@ -112,22 +114,26 @@ class ExportManager
*
* @internal This class check the interface implemented by export, and, if ´ListInterface´ is used, return an empty array
*
* @return iterable<string, AggregatorInterface>|null a \Generator that contains aggretagors. The key is the filter's alias
* @return array<string, AggregatorInterface> an array that contains aggregators. The key is the filter's alias
*/
public function &getAggregatorsApplyingOn(DirectExportInterface|ExportInterface $export, array $centers = null): ?iterable
public function getAggregatorsApplyingOn(DirectExportInterface|ExportInterface $export, array $centers = null): array
{
if ($export instanceof ListInterface || $export instanceof DirectExportInterface) {
return;
return [];
}
$aggregators = [];
foreach ($this->aggregators as $alias => $aggregator) {
if (
\in_array($aggregator->applyOn(), $export->supportsModifiers(), true)
&& $this->isGrantedForElement($aggregator, $export, $centers)
) {
yield $alias => $aggregator;
$aggregators[$alias] = $aggregator;
}
}
return $aggregators;
}
public function addExportElementsProvider(ExportElementsProviderInterface $provider, string $prefix): void
@@ -347,6 +353,17 @@ class ExportManager
return $this->filters[$alias];
}
public function getAllFilters(): array
{
$filters = [];
foreach ($this->filters as $alias => $filter) {
$filters[$alias] = $filter;
}
return $filters;
}
/**
* get all filters.
*
@@ -452,7 +469,7 @@ class ExportManager
if (null === $centers || [] === $centers) {
// we want to try if at least one center is reachable
return [] !== $this->authorizationHelper->getReachableCenters(
$this->user,
$this->tokenStorage->getToken()->getUser(),
$role
);
}
@@ -484,11 +501,17 @@ class ExportManager
{
$r = [];
$user = $this->tokenStorage->getToken()->getUser();
if (!$user instanceof User) {
return [];
}
foreach ($centers as $center) {
$r[] = [
'center' => $center,
'circles' => $this->authorizationHelper->getReachableScopes(
$this->user,
$user,
$element->requiredRole(),
$center
),

View File

@@ -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\Export;
use Symfony\Contracts\Translation\TranslatorInterface;
final readonly class SortExportElement
{
public function __construct(
private TranslatorInterface $translator,
) {}
/**
* @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()));
}
/**
* @param array<int|string, AggregatorInterface> $elements
*/
public function sortAggregators(array &$elements): void
{
uasort($elements, fn (AggregatorInterface $a, AggregatorInterface $b) => $this->translator->trans($a->getTitle()) <=> $this->translator->trans($b->getTitle()));
}
}

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form\Type\Export;
use Chill\MainBundle\Export\ExportManager;
use Chill\MainBundle\Export\SortExportElement;
use Chill\MainBundle\Validator\Constraints\Export\ExportElementConstraint;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
@@ -28,15 +29,7 @@ class ExportType extends AbstractType
final public const PICK_FORMATTER_KEY = 'pick_formatter';
/**
* @var ExportManager
*/
protected $exportManager;
public function __construct(ExportManager $exportManager)
{
$this->exportManager = $exportManager;
}
public function __construct(private readonly ExportManager $exportManager, private readonly SortExportElement $sortExportElement) {}
public function buildForm(FormBuilderInterface $builder, array $options)
{
@@ -58,12 +51,12 @@ class ExportType extends AbstractType
if ($export instanceof \Chill\MainBundle\Export\ExportInterface) {
// add filters
$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) {
$filterBuilder->add($alias, FilterType::class, [
'filter_alias' => $alias,
'export_manager' => $this->exportManager,
'filter' => $filter,
'label' => $filter->getTitle(),
'constraints' => [
new ExportElementConstraint(['element' => $filter]),
@@ -76,6 +69,7 @@ class ExportType extends AbstractType
// add aggregators
$aggregators = $this->exportManager
->getAggregatorsApplyingOn($export, $options['picked_centers']);
$this->sortExportElement->sortAggregators($aggregators);
$aggregatorBuilder = $builder->create(
self::AGGREGATOR_KEY,
FormType::class,

View File

@@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form\Type\Export;
use Chill\MainBundle\Export\FilterInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
@@ -25,8 +26,7 @@ class FilterType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$exportManager = $options['export_manager'];
$filter = $exportManager->getFilter($options['filter_alias']);
$filter = $options['filter'];
$builder
->add(self::ENABLED_FIELD, CheckboxType::class, [
@@ -46,8 +46,9 @@ class FilterType extends AbstractType
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setRequired('filter_alias')
->setRequired('export_manager')
$resolver
->setRequired('filter')
->setAllowedTypes('filter', [FilterInterface::class])
->setDefault('compound', true)
->setDefault('error_bubbling', false);
}

View File

@@ -0,0 +1,193 @@
<?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\Export\AggregatorInterface;
use Chill\MainBundle\Export\ExportManager;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Export\SortExportElement;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @internal
*
* @coversNothing
*/
class SortExportElementTest extends KernelTestCase
{
private SortExportElement $sortExportElement;
public function setUp(): void
{
parent::setUpBeforeClass();
$this->sortExportElement = new SortExportElement($this->makeTranslator());
}
public function testSortFilterRealData(): void
{
self::bootKernel();
$sorter = self::$container->get(SortExportElement::class);
$translator = self::$container->get(TranslatorInterface::class);
$exportManager = self::$container->get(ExportManager::class);
$filters = $exportManager->getAllFilters();
$sorter->sortFilters($filters);
$previousName = null;
foreach ($filters as $filter) {
if (null === $previousName) {
$previousName = $translator->trans($filter->getTitle());
continue;
}
$current = $translator->trans($filter->getTitle());
if ($current === $previousName) {
continue;
}
self::assertEquals(-1, $previousName <=> $current, sprintf("comparing '%s' and '%s'", $previousName, $current));
$previousName = $current;
}
}
public function testSortAggregator(): void
{
$aggregators = [
'foo' => $a = $this->makeAggregator('a'),
'zop' => $q = $this->makeAggregator('q'),
'bar' => $c = $this->makeAggregator('c'),
'baz' => $b = $this->makeAggregator('b'),
];
$this->sortExportElement->sortAggregators($aggregators);
self::assertEquals(['foo', 'baz', 'bar', 'zop'], array_keys($aggregators));
self::assertSame($a, $aggregators['foo']);
self::assertSame($b, $aggregators['baz']);
self::assertSame($c, $aggregators['bar']);
self::assertSame($q, $aggregators['zop']);
}
public function testSortFilter(): void
{
$filters = [
'foo' => $a = $this->makeFilter('a'),
'zop' => $q = $this->makeFilter('q'),
'bar' => $c = $this->makeFilter('c'),
'baz' => $b = $this->makeFilter('b'),
];
$this->sortExportElement->sortFilters($filters);
self::assertEquals(['foo', 'baz', 'bar', 'zop'], array_keys($filters));
self::assertSame($a, $filters['foo']);
self::assertSame($b, $filters['baz']);
self::assertSame($c, $filters['bar']);
self::assertSame($q, $filters['zop']);
}
private function makeTranslator(): TranslatorInterface
{
return new class () implements TranslatorInterface {
public function trans(string $id, array $parameters = [], string $domain = null, string $locale = null)
{
return $id;
}
public function getLocale(): string
{
return 'en';
}
};
}
private function makeAggregator(string $title): AggregatorInterface
{
return new class ($title) implements AggregatorInterface {
public function __construct(private readonly string $title) {}
public function buildForm(FormBuilderInterface $builder) {}
public function getFormDefaultData(): array
{
return [];
}
public function getLabels($key, array $values, mixed $data)
{
return fn ($v) => $v;
}
public function getQueryKeys($data)
{
return [];
}
public function getTitle()
{
return $this->title;
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data) {}
public function applyOn()
{
return [];
}
};
}
private function makeFilter(string $title): FilterInterface
{
return new class ($title) implements FilterInterface {
public function __construct(private readonly string $title) {}
public function getTitle()
{
return $this->title;
}
public function buildForm(FormBuilderInterface $builder) {}
public function getFormDefaultData(): array
{
return [];
}
public function describeAction($data, $format = 'string')
{
return ['a', []];
}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data) {}
public function applyOn()
{
return [];
}
};
}
}

View File

@@ -54,3 +54,5 @@ services:
- { name: chill.export_formatter, alias: 'csv_pivoted_list' }
Chill\MainBundle\Export\AccompanyingCourseExportHelper: ~
Chill\MainBundle\Export\SortExportElement: ~