[export] sort filters and aggregators by title

This commit is contained in:
Julien Fastré 2023-10-19 14:04:24 +02:00
parent 4c9ea740c8
commit 5f805626f7
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
11 changed files with 294 additions and 38 deletions

View File

@ -0,0 +1,5 @@
kind: Feature
body: '[export] sort filters and aggregators by title'
time: 2023-10-19T14:03:57.435338127+02:00
custom:
Issue: ""

View File

@ -362,7 +362,7 @@ export:
Filter by users scope: Filtrer les échanges par services d'au moins un utilisateur participant
'Filtered activity by users scope: only %scopes%': 'Filtré par service d''au moins un utilisateur participant: seulement %scopes%'
course_having_activity_between_date:
Title: Filtre les parcours ayant reçu un échange entre deux dates
Title: Filtrer les parcours ayant reçu un échange entre deux dates
Receiving an activity after: Ayant reçu un échange après le
Receiving an activity before: Ayant reçu un échange avant le
acp_by_activity_type:
@ -372,7 +372,7 @@ export:
Implied in an activity before this date: Impliqué dans un échange avant cette date
Activity reasons for those activities: Sujets de ces échanges
if no reasons: Si aucun sujet n'est coché, tous les sujets seront pris en compte
title: Filtrer les personnes ayant été associés à un échange au cours de la période
title: Filtrer les usagers ayant été associés à un échange au cours de la période
date mismatch: La date de fin de la période doit être supérieure à la date du début
by_creator_scope:
Filter activity by user scope: Filtrer les échanges par service du créateur de l'échange

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: ~

View File

@ -66,7 +66,7 @@ class AddressRefStatusFilter implements \Chill\MainBundle\Export\FilterInterface
{
$builder
->add('date_calc', PickRollingDateType::class, [
'label' => 'Compute address at date',
'label' => 'export.filter.person.by_address_ref_status.Address at date',
'required' => true,
])
->add('ref_statuses', ChoiceType::class, [

View File

@ -480,7 +480,7 @@ acp_geog_agg_unitrefid: Clé de la zone géographique
Geographical layer: Couche géographique
Select a geographical layer: Choisir une couche géographique
Group people by geographical unit based on his address: Grouper les usagers par zone géographique (sur base de l'adresse)
Filter by person's geographical unit (based on address): Filter les usagers par zone géographique (sur base de l'adresse)
Filter by person's geographical unit (based on address): Filtrer les usagers par zone géographique (sur base de l'adresse)
Filter by socialaction: Filtrer les parcours par action d'accompagnement
Accepted socialactions: Actions d'accompagnement
@ -1100,21 +1100,22 @@ export:
Persons filtered by no composition at %date%: Uniquement les usagers sans composition de ménage à la date du %date%
Date calc: Date de calcul
by_address_ref_status:
Filter by person's address ref status: Filtrer par comparaison avec l'adresse de référence
Filter by person's address ref status: Filtrer les usagers par comparaison avec l'adresse de référence
to_review: Diffère de l'adresse de référence
reviewed: Diffère de l'adresse de référence mais conservé par l'utilisateur
match: Identique à l'adresse de référence
Filtered by person\'s address status computed at %datecalc%, only %statuses%: Filtré par comparaison à l'adresse de référence, calculé à %datecalc%, seulement %statuses%
Status: Statut
Address at date: Adresse à la date
course:
having_info_within_interval:
title: Filter les parcours ayant reçu une intervention entre deux dates
title: Filtrer les parcours ayant reçu une intervention entre deux dates
start_date: Début de la période
end_date: Fin de la période
Only course with events between %startDate% and %endDate%: Seulement les parcours ayant reçu une intervention entre le %startDate% et le %endDate%
by_user_working:
title: Filter les parcours par intervenant, entre deux dates
title: Filtrer les parcours par intervenant, entre deux dates
'Filtered by user working on course: only %users%, between %start_date% and %end_date%': 'Filtré par intervenants sur le parcours: seulement %users%, entre le %start_date% et le %end_date%'
User working after: Intervention après le
User working before: Intervention avant le

View File

@ -128,8 +128,8 @@ export:
thirdParties: Tiers intervenant
# exports filters/aggregators
Filtered by person\'s who have a residential address located at a thirdparty of type %thirparty_type%: Uniquement les personnes qui ont une addresse de résidence chez un tiers de catégorie %thirdparty_type%
Filtered by person\'s who have a residential address located at a thirdparty of type %thirparty_type%: Uniquement les usagers qui ont une addresse de résidence chez un tiers de catégorie %thirdparty_type%
is thirdparty: Le demandeur est un tiers
Filter by person's who have a residential address located at a thirdparty of type: Filtrer les personnes qui ont une addresse de résidence chez un tiers de catégorie "xxx"
"Filtered by person's who have a residential address located at a thirdparty of type %thirdparty_type% and valid on %date_calc%": "Uniquement les personnes qui ont une addresse de résidence chez un tiers de catégorie %thirdparty_type% et valide sur la date %date_calc%"
Filter by person's who have a residential address located at a thirdparty of type: Filtrer les usagers qui ont une addresse de résidence chez un tiers de catégorie "xxx"
"Filtered by person's who have a residential address located at a thirdparty of type %thirdparty_type% and valid on %date_calc%": "Uniquement les usagers qui ont une addresse de résidence chez un tiers de catégorie %thirdparty_type% et valide sur la date %date_calc%"