From 5f805626f76d8fc40dcb807239f995071ca5c612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 19 Oct 2023 14:04:24 +0200 Subject: [PATCH] [export] sort filters and aggregators by title --- .../unreleased/Feature-20231019-140357.yaml | 5 + .../translations/messages.fr.yml | 4 +- .../ChillMainBundle/Export/ExportManager.php | 49 +++-- .../Export/SortExportElement.php | 37 ++++ .../Form/Type/Export/ExportType.php | 16 +- .../Form/Type/Export/FilterType.php | 9 +- .../Tests/Export/SortExportElementTest.php | 193 ++++++++++++++++++ .../config/services/export.yaml | 2 + .../PersonFilters/AddressRefStatusFilter.php | 2 +- .../translations/messages.fr.yml | 9 +- .../translations/messages.fr.yml | 6 +- 11 files changed, 294 insertions(+), 38 deletions(-) create mode 100644 .changes/unreleased/Feature-20231019-140357.yaml create mode 100644 src/Bundle/ChillMainBundle/Export/SortExportElement.php create mode 100644 src/Bundle/ChillMainBundle/Tests/Export/SortExportElementTest.php diff --git a/.changes/unreleased/Feature-20231019-140357.yaml b/.changes/unreleased/Feature-20231019-140357.yaml new file mode 100644 index 000000000..2ef33a4a5 --- /dev/null +++ b/.changes/unreleased/Feature-20231019-140357.yaml @@ -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: "" diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml index b952ce3a8..a7112b534 100644 --- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml @@ -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 diff --git a/src/Bundle/ChillMainBundle/Export/ExportManager.php b/src/Bundle/ChillMainBundle/Export/ExportManager.php index 8cb69bd63..067cc72e9 100644 --- a/src/Bundle/ChillMainBundle/Export/ExportManager.php +++ b/src/Bundle/ChillMainBundle/Export/ExportManager.php @@ -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|null a \Generator that contains aggretagors. The key is the filter's alias + * @return array 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 ), diff --git a/src/Bundle/ChillMainBundle/Export/SortExportElement.php b/src/Bundle/ChillMainBundle/Export/SortExportElement.php new file mode 100644 index 000000000..6228109ed --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/SortExportElement.php @@ -0,0 +1,37 @@ + $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 $elements + */ + public function sortAggregators(array &$elements): void + { + uasort($elements, fn (AggregatorInterface $a, AggregatorInterface $b) => $this->translator->trans($a->getTitle()) <=> $this->translator->trans($b->getTitle())); + } +} diff --git a/src/Bundle/ChillMainBundle/Form/Type/Export/ExportType.php b/src/Bundle/ChillMainBundle/Form/Type/Export/ExportType.php index 324b5d8d7..e5d0887f3 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Export/ExportType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Export/ExportType.php @@ -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, diff --git a/src/Bundle/ChillMainBundle/Form/Type/Export/FilterType.php b/src/Bundle/ChillMainBundle/Form/Type/Export/FilterType.php index d4f897430..bcf842e73 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Export/FilterType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Export/FilterType.php @@ -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); } diff --git a/src/Bundle/ChillMainBundle/Tests/Export/SortExportElementTest.php b/src/Bundle/ChillMainBundle/Tests/Export/SortExportElementTest.php new file mode 100644 index 000000000..e7dbaf8fb --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Export/SortExportElementTest.php @@ -0,0 +1,193 @@ +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 []; + } + }; + } +} diff --git a/src/Bundle/ChillMainBundle/config/services/export.yaml b/src/Bundle/ChillMainBundle/config/services/export.yaml index 6bae6f9c0..ece7ae902 100644 --- a/src/Bundle/ChillMainBundle/config/services/export.yaml +++ b/src/Bundle/ChillMainBundle/config/services/export.yaml @@ -54,3 +54,5 @@ services: - { name: chill.export_formatter, alias: 'csv_pivoted_list' } Chill\MainBundle\Export\AccompanyingCourseExportHelper: ~ + + Chill\MainBundle\Export\SortExportElement: ~ diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/AddressRefStatusFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/AddressRefStatusFilter.php index 6430b5e99..8cd675abe 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/AddressRefStatusFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/AddressRefStatusFilter.php @@ -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, [ diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 4ab5d88a6..66aecff1f 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -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 diff --git a/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml b/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml index f62a5beff..2bb17cca4 100644 --- a/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillThirdPartyBundle/translations/messages.fr.yml @@ -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%"