[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 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%' 'Filtered activity by users scope: only %scopes%': 'Filtré par service d''au moins un utilisateur participant: seulement %scopes%'
course_having_activity_between_date: 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 after: Ayant reçu un échange après le
Receiving an activity before: Ayant reçu un échange avant le Receiving an activity before: Ayant reçu un échange avant le
acp_by_activity_type: acp_by_activity_type:
@ -372,7 +372,7 @@ export:
Implied in an activity before this date: Impliqué dans un échange avant cette date Implied in an activity before this date: Impliqué dans un échange avant cette date
Activity reasons for those activities: Sujets de ces échanges 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 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 date mismatch: La date de fin de la période doit être supérieure à la date du début
by_creator_scope: by_creator_scope:
Filter activity by user scope: Filtrer les échanges par service du créateur de l'échange 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; namespace Chill\MainBundle\Export;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\Type\Export\ExportType; use Chill\MainBundle\Form\Type\Export\ExportType;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface; use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
@ -54,20 +55,17 @@ class ExportManager
*/ */
private array $formatters = []; private array $formatters = [];
private readonly string|\Stringable|\Symfony\Component\Security\Core\User\UserInterface $user;
public function __construct( public function __construct(
private readonly LoggerInterface $logger, private readonly LoggerInterface $logger,
private readonly AuthorizationCheckerInterface $authorizationChecker, private readonly AuthorizationCheckerInterface $authorizationChecker,
private readonly AuthorizationHelperInterface $authorizationHelper, private readonly AuthorizationHelperInterface $authorizationHelper,
TokenStorageInterface $tokenStorage, private readonly TokenStorageInterface $tokenStorage,
iterable $exports, iterable $exports,
iterable $aggregators, iterable $aggregators,
iterable $filters iterable $filters
// iterable $formatters, // iterable $formatters,
// iterable $exportElementProvider // iterable $exportElementProvider
) { ) {
$this->user = $tokenStorage->getToken()->getUser();
$this->exports = iterator_to_array($exports); $this->exports = iterator_to_array($exports);
$this->aggregators = iterator_to_array($aggregators); $this->aggregators = iterator_to_array($aggregators);
$this->filters = iterator_to_array($filters); $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 * @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) { if ($export instanceof DirectExportInterface) {
return; return [];
} }
$filters = [];
foreach ($this->filters as $alias => $filter) { foreach ($this->filters as $alias => $filter) {
if ( if (
\in_array($filter->applyOn(), $export->supportsModifiers(), true) \in_array($filter->applyOn(), $export->supportsModifiers(), true)
&& $this->isGrantedForElement($filter, $export, $centers) && $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 * @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) { if ($export instanceof ListInterface || $export instanceof DirectExportInterface) {
return; return [];
} }
$aggregators = [];
foreach ($this->aggregators as $alias => $aggregator) { foreach ($this->aggregators as $alias => $aggregator) {
if ( if (
\in_array($aggregator->applyOn(), $export->supportsModifiers(), true) \in_array($aggregator->applyOn(), $export->supportsModifiers(), true)
&& $this->isGrantedForElement($aggregator, $export, $centers) && $this->isGrantedForElement($aggregator, $export, $centers)
) { ) {
yield $alias => $aggregator; $aggregators[$alias] = $aggregator;
} }
} }
return $aggregators;
} }
public function addExportElementsProvider(ExportElementsProviderInterface $provider, string $prefix): void public function addExportElementsProvider(ExportElementsProviderInterface $provider, string $prefix): void
@ -347,6 +353,17 @@ class ExportManager
return $this->filters[$alias]; return $this->filters[$alias];
} }
public function getAllFilters(): array
{
$filters = [];
foreach ($this->filters as $alias => $filter) {
$filters[$alias] = $filter;
}
return $filters;
}
/** /**
* get all filters. * get all filters.
* *
@ -452,7 +469,7 @@ class ExportManager
if (null === $centers || [] === $centers) { if (null === $centers || [] === $centers) {
// we want to try if at least one center is reachable // we want to try if at least one center is reachable
return [] !== $this->authorizationHelper->getReachableCenters( return [] !== $this->authorizationHelper->getReachableCenters(
$this->user, $this->tokenStorage->getToken()->getUser(),
$role $role
); );
} }
@ -484,11 +501,17 @@ class ExportManager
{ {
$r = []; $r = [];
$user = $this->tokenStorage->getToken()->getUser();
if (!$user instanceof User) {
return [];
}
foreach ($centers as $center) { foreach ($centers as $center) {
$r[] = [ $r[] = [
'center' => $center, 'center' => $center,
'circles' => $this->authorizationHelper->getReachableScopes( 'circles' => $this->authorizationHelper->getReachableScopes(
$this->user, $user,
$element->requiredRole(), $element->requiredRole(),
$center $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; namespace Chill\MainBundle\Form\Type\Export;
use Chill\MainBundle\Export\ExportManager; use Chill\MainBundle\Export\ExportManager;
use Chill\MainBundle\Export\SortExportElement;
use Chill\MainBundle\Validator\Constraints\Export\ExportElementConstraint; use Chill\MainBundle\Validator\Constraints\Export\ExportElementConstraint;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\FormType;
@ -28,15 +29,7 @@ class ExportType extends AbstractType
final public const PICK_FORMATTER_KEY = 'pick_formatter'; final public const PICK_FORMATTER_KEY = 'pick_formatter';
/** public function __construct(private readonly ExportManager $exportManager, private readonly SortExportElement $sortExportElement) {}
* @var ExportManager
*/
protected $exportManager;
public function __construct(ExportManager $exportManager)
{
$this->exportManager = $exportManager;
}
public function buildForm(FormBuilderInterface $builder, array $options) public function buildForm(FormBuilderInterface $builder, array $options)
{ {
@ -58,12 +51,12 @@ class ExportType extends AbstractType
if ($export instanceof \Chill\MainBundle\Export\ExportInterface) { if ($export instanceof \Chill\MainBundle\Export\ExportInterface) {
// add filters // add filters
$filters = $this->exportManager->getFiltersApplyingOn($export, $options['picked_centers']); $filters = $this->exportManager->getFiltersApplyingOn($export, $options['picked_centers']);
$this->sortExportElement->sortFilters($filters);
$filterBuilder = $builder->create(self::FILTER_KEY, FormType::class, ['compound' => true]); $filterBuilder = $builder->create(self::FILTER_KEY, FormType::class, ['compound' => true]);
foreach ($filters as $alias => $filter) { foreach ($filters as $alias => $filter) {
$filterBuilder->add($alias, FilterType::class, [ $filterBuilder->add($alias, FilterType::class, [
'filter_alias' => $alias, 'filter' => $filter,
'export_manager' => $this->exportManager,
'label' => $filter->getTitle(), 'label' => $filter->getTitle(),
'constraints' => [ 'constraints' => [
new ExportElementConstraint(['element' => $filter]), new ExportElementConstraint(['element' => $filter]),
@ -76,6 +69,7 @@ class ExportType extends AbstractType
// add aggregators // add aggregators
$aggregators = $this->exportManager $aggregators = $this->exportManager
->getAggregatorsApplyingOn($export, $options['picked_centers']); ->getAggregatorsApplyingOn($export, $options['picked_centers']);
$this->sortExportElement->sortAggregators($aggregators);
$aggregatorBuilder = $builder->create( $aggregatorBuilder = $builder->create(
self::AGGREGATOR_KEY, self::AGGREGATOR_KEY,
FormType::class, FormType::class,

View File

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

View File

@ -66,7 +66,7 @@ class AddressRefStatusFilter implements \Chill\MainBundle\Export\FilterInterface
{ {
$builder $builder
->add('date_calc', PickRollingDateType::class, [ ->add('date_calc', PickRollingDateType::class, [
'label' => 'Compute address at date', 'label' => 'export.filter.person.by_address_ref_status.Address at date',
'required' => true, 'required' => true,
]) ])
->add('ref_statuses', ChoiceType::class, [ ->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 Geographical layer: Couche géographique
Select a geographical layer: Choisir une 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) 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 Filter by socialaction: Filtrer les parcours par action d'accompagnement
Accepted socialactions: Actions 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% Persons filtered by no composition at %date%: Uniquement les usagers sans composition de ménage à la date du %date%
Date calc: Date de calcul Date calc: Date de calcul
by_address_ref_status: 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 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 reviewed: Diffère de l'adresse de référence mais conservé par l'utilisateur
match: Identique à l'adresse de référence 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% 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 Status: Statut
Address at date: Adresse à la date
course: course:
having_info_within_interval: 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 start_date: Début de la période
end_date: Fin 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% 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: 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%' '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 after: Intervention après le
User working before: Intervention avant le User working before: Intervention avant le

View File

@ -128,8 +128,8 @@ export:
thirdParties: Tiers intervenant thirdParties: Tiers intervenant
# exports filters/aggregators # 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 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" 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 personnes qui ont une addresse de résidence chez un tiers de catégorie %thirdparty_type% et valide sur la date %date_calc%" "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%"