From 87a6757e5e41f823d3ca6824e08ac914170d3ece Mon Sep 17 00:00:00 2001 From: LenaertsJ Date: Tue, 25 Jun 2024 11:24:57 +0000 Subject: [PATCH 01/13] Add eventBundle exports --- composer.json | 2 +- rector.php | 8 +- .../Controller/EventController.php | 3 +- .../Controller/ParticipationController.php | 2 +- .../ChillEventExtension.php | 7 +- .../Export/Aggregator/EventDateAggregator.php | 110 +++++++++++++++ .../Export/Aggregator/EventTypeAggregator.php | 83 ++++++++++++ .../Export/Aggregator/RoleAggregator.php | 83 ++++++++++++ .../ChillEventBundle/Export/Declarations.php | 22 +++ .../Export/CountEventParticipations.php | 127 +++++++++++++++++ .../Export/Export/CountEvents.php | 128 ++++++++++++++++++ .../Export/Filter/EventDateFilter.php | 97 +++++++++++++ .../Export/Filter/EventTypeFilter.php | 95 +++++++++++++ .../Export/Filter/RoleFilter.php | 95 +++++++++++++ .../Menu/PersonMenuBuilder.php | 2 +- .../Menu/SectionMenuBuilder.php | 2 +- .../Repository/EventACLAwareRepository.php | 2 +- .../Repository/RoleRepository.php | 54 +++++++- .../{Authorization => }/EventVoter.php | 64 +++------ .../ParticipationVoter.php | 59 +++----- .../Export/CountEventParticipationsTest.php | 43 ++++++ .../Tests/Export/CountEventTest.php | 43 ++++++ .../aggregators/EventDateAggregatorTest.php | 59 ++++++++ .../aggregators/EventTypeAggregatorTest.php | 59 ++++++++ .../Export/aggregators/RoleAggregatorTest.php | 63 +++++++++ .../Export/filters/EventDateFilterTest.php | 65 +++++++++ .../Export/filters/EventTypeFilterTest.php | 76 +++++++++++ .../Tests/Export/filters/RoleFilterTest.php | 81 +++++++++++ .../EventACLAwareRepositoryTest.php | 2 +- .../config/services/authorization.yaml | 20 --- .../config/services/export.yaml | 41 ++++++ .../config/services/security.yaml | 14 ++ .../translations/messages.fr.yml | 22 +++ 33 files changed, 1512 insertions(+), 121 deletions(-) create mode 100644 src/Bundle/ChillEventBundle/Export/Aggregator/EventDateAggregator.php create mode 100644 src/Bundle/ChillEventBundle/Export/Aggregator/EventTypeAggregator.php create mode 100644 src/Bundle/ChillEventBundle/Export/Aggregator/RoleAggregator.php create mode 100644 src/Bundle/ChillEventBundle/Export/Declarations.php create mode 100644 src/Bundle/ChillEventBundle/Export/Export/CountEventParticipations.php create mode 100644 src/Bundle/ChillEventBundle/Export/Export/CountEvents.php create mode 100644 src/Bundle/ChillEventBundle/Export/Filter/EventDateFilter.php create mode 100644 src/Bundle/ChillEventBundle/Export/Filter/EventTypeFilter.php create mode 100644 src/Bundle/ChillEventBundle/Export/Filter/RoleFilter.php rename src/Bundle/ChillEventBundle/Security/{Authorization => }/EventVoter.php (60%) rename src/Bundle/ChillEventBundle/Security/{Authorization => }/ParticipationVoter.php (62%) create mode 100644 src/Bundle/ChillEventBundle/Tests/Export/CountEventParticipationsTest.php create mode 100644 src/Bundle/ChillEventBundle/Tests/Export/CountEventTest.php create mode 100644 src/Bundle/ChillEventBundle/Tests/Export/aggregators/EventDateAggregatorTest.php create mode 100644 src/Bundle/ChillEventBundle/Tests/Export/aggregators/EventTypeAggregatorTest.php create mode 100644 src/Bundle/ChillEventBundle/Tests/Export/aggregators/RoleAggregatorTest.php create mode 100644 src/Bundle/ChillEventBundle/Tests/Export/filters/EventDateFilterTest.php create mode 100644 src/Bundle/ChillEventBundle/Tests/Export/filters/EventTypeFilterTest.php create mode 100644 src/Bundle/ChillEventBundle/Tests/Export/filters/RoleFilterTest.php delete mode 100644 src/Bundle/ChillEventBundle/config/services/authorization.yaml create mode 100644 src/Bundle/ChillEventBundle/config/services/export.yaml create mode 100644 src/Bundle/ChillEventBundle/config/services/security.yaml diff --git a/composer.json b/composer.json index 37282788e..f08f94677 100644 --- a/composer.json +++ b/composer.json @@ -76,7 +76,7 @@ "phpunit/phpunit": ">= 7.5", "psalm/plugin-phpunit": "^0.18.4", "psalm/plugin-symfony": "^4.0.2", - "rector/rector": "^1.1.0", + "rector/rector": "1.1.1", "symfony/debug-bundle": "^5.1", "symfony/dotenv": "^4.4", "symfony/maker-bundle": "^1.20", diff --git a/rector.php b/rector.php index 8aa62bdd0..0e6961bb6 100644 --- a/rector.php +++ b/rector.php @@ -32,9 +32,13 @@ return static function (RectorConfig $rectorConfig): void { //define sets of rules $rectorConfig->sets([ LevelSetList::UP_TO_PHP_82, - \Rector\Symfony\Set\SymfonyLevelSetList::UP_TO_SYMFONY_44, + \Rector\Symfony\Set\SymfonySetList::SYMFONY_40, + \Rector\Symfony\Set\SymfonySetList::SYMFONY_41, + \Rector\Symfony\Set\SymfonySetList::SYMFONY_42, + \Rector\Symfony\Set\SymfonySetList::SYMFONY_43, + \Rector\Symfony\Set\SymfonySetList::SYMFONY_44, \Rector\Doctrine\Set\DoctrineSetList::DOCTRINE_CODE_QUALITY, - \Rector\PHPUnit\Set\PHPUnitLevelSetList::UP_TO_PHPUNIT_90, + \Rector\PHPUnit\Set\PHPUnitSetList::PHPUNIT_90, ]); // some routes are added twice if it remains activated diff --git a/src/Bundle/ChillEventBundle/Controller/EventController.php b/src/Bundle/ChillEventBundle/Controller/EventController.php index e49c02c06..4a000eefc 100644 --- a/src/Bundle/ChillEventBundle/Controller/EventController.php +++ b/src/Bundle/ChillEventBundle/Controller/EventController.php @@ -15,7 +15,7 @@ use Chill\EventBundle\Entity\Event; use Chill\EventBundle\Entity\Participation; use Chill\EventBundle\Form\EventType; use Chill\EventBundle\Form\Type\PickEventType; -use Chill\EventBundle\Security\Authorization\EventVoter; +use Chill\EventBundle\Security\EventVoter; use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Pagination\PaginatorFactory; @@ -433,7 +433,6 @@ final class EventController extends AbstractController $builder->add('event_id', HiddenType::class, [ 'data' => $event->getId(), ]); - dump($event->getId()); return $builder->getForm(); } diff --git a/src/Bundle/ChillEventBundle/Controller/ParticipationController.php b/src/Bundle/ChillEventBundle/Controller/ParticipationController.php index 06a8dda29..2c20de7e9 100644 --- a/src/Bundle/ChillEventBundle/Controller/ParticipationController.php +++ b/src/Bundle/ChillEventBundle/Controller/ParticipationController.php @@ -15,7 +15,7 @@ use Chill\EventBundle\Entity\Event; use Chill\EventBundle\Entity\Participation; use Chill\EventBundle\Form\ParticipationType; use Chill\EventBundle\Repository\EventRepository; -use Chill\EventBundle\Security\Authorization\ParticipationVoter; +use Chill\EventBundle\Security\ParticipationVoter; use Chill\PersonBundle\Repository\PersonRepository; use Chill\PersonBundle\Security\Authorization\PersonVoter; use Doctrine\Common\Collections\Collection; diff --git a/src/Bundle/ChillEventBundle/DependencyInjection/ChillEventExtension.php b/src/Bundle/ChillEventBundle/DependencyInjection/ChillEventExtension.php index 8ddcab58c..0b30ca6c5 100644 --- a/src/Bundle/ChillEventBundle/DependencyInjection/ChillEventExtension.php +++ b/src/Bundle/ChillEventBundle/DependencyInjection/ChillEventExtension.php @@ -11,8 +11,8 @@ declare(strict_types=1); namespace Chill\EventBundle\DependencyInjection; -use Chill\EventBundle\Security\Authorization\EventVoter; -use Chill\EventBundle\Security\Authorization\ParticipationVoter; +use Chill\EventBundle\Security\EventVoter; +use Chill\EventBundle\Security\ParticipationVoter; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; @@ -33,12 +33,13 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config')); $loader->load('services.yaml'); - $loader->load('services/authorization.yaml'); + $loader->load('services/security.yaml'); $loader->load('services/fixtures.yaml'); $loader->load('services/forms.yaml'); $loader->load('services/repositories.yaml'); $loader->load('services/search.yaml'); $loader->load('services/timeline.yaml'); + $loader->load('services/export.yaml'); } /** (non-PHPdoc). diff --git a/src/Bundle/ChillEventBundle/Export/Aggregator/EventDateAggregator.php b/src/Bundle/ChillEventBundle/Export/Aggregator/EventDateAggregator.php new file mode 100644 index 000000000..1e519997a --- /dev/null +++ b/src/Bundle/ChillEventBundle/Export/Aggregator/EventDateAggregator.php @@ -0,0 +1,110 @@ + 'month', + 'by week' => 'week', + 'by year' => 'year', + ]; + + private const DEFAULT_CHOICE = 'year'; + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $order = null; + + switch ($data['frequency']) { + case 'month': + $fmt = 'YYYY-MM'; + + break; + + case 'week': + $fmt = 'YYYY-IW'; + + break; + + case 'year': + $fmt = 'YYYY'; + $order = 'DESC'; + + break; + + default: + throw new \RuntimeException(sprintf("The frequency data '%s' is invalid.", $data['frequency'])); + } + + $qb->addSelect(sprintf("TO_CHAR(event.date, '%s') AS date_aggregator", $fmt)); + $qb->addGroupBy('date_aggregator'); + $qb->addOrderBy('date_aggregator', $order); + } + + public function applyOn(): string + { + return Declarations::EVENT; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('frequency', ChoiceType::class, [ + 'choices' => self::CHOICES, + 'multiple' => false, + 'expanded' => true, + ]); + } + + public function getFormDefaultData(): array + { + return ['frequency' => self::DEFAULT_CHOICE]; + } + + public function getLabels($key, array $values, $data) + { + return static function ($value) use ($data): string { + if ('_header' === $value) { + return 'by '.$data['frequency']; + } + + if (null === $value) { + return ''; + } + + return match ($data['frequency']) { + default => $value, + }; + }; + } + + public function getQueryKeys($data): array + { + return ['date_aggregator']; + } + + public function getTitle(): string + { + return 'Group event by date'; + } +} diff --git a/src/Bundle/ChillEventBundle/Export/Aggregator/EventTypeAggregator.php b/src/Bundle/ChillEventBundle/Export/Aggregator/EventTypeAggregator.php new file mode 100644 index 000000000..c0010d77a --- /dev/null +++ b/src/Bundle/ChillEventBundle/Export/Aggregator/EventTypeAggregator.php @@ -0,0 +1,83 @@ +getAllAliases(), true)) { + $qb->leftJoin('event.type', 'eventtype'); + } + + $qb->addSelect(sprintf('IDENTITY(event.type) AS %s', self::KEY)); + $qb->addGroupBy(self::KEY); + } + + public function applyOn(): string + { + return Declarations::EVENT; + } + + public function buildForm(FormBuilderInterface $builder) + { + // no form required for this aggregator + } + + public function getFormDefaultData(): array + { + return []; + } + + public function getLabels($key, array $values, $data): \Closure + { + return function (int|string|null $value): string { + if ('_header' === $value) { + return 'Event type'; + } + + if (null === $value || '' === $value || null === $t = $this->eventTypeRepository->find($value)) { + return ''; + } + + return $this->translatableStringHelper->localize($t->getName()); + }; + } + + public function getQueryKeys($data): array + { + return [self::KEY]; + } + + public function getTitle() + { + return 'Group by event type'; + } +} diff --git a/src/Bundle/ChillEventBundle/Export/Aggregator/RoleAggregator.php b/src/Bundle/ChillEventBundle/Export/Aggregator/RoleAggregator.php new file mode 100644 index 000000000..f8d1985e2 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Export/Aggregator/RoleAggregator.php @@ -0,0 +1,83 @@ +getAllAliases(), true)) { + $qb->leftJoin('event_part.role', 'role'); + } + + $qb->addSelect(sprintf('IDENTITY(event_part.role) AS %s', self::KEY)); + $qb->addGroupBy(self::KEY); + } + + public function applyOn(): string + { + return Declarations::EVENT_PARTICIPANTS; + } + + public function buildForm(FormBuilderInterface $builder) + { + // no form required for this aggregator + } + + public function getFormDefaultData(): array + { + return []; + } + + public function getLabels($key, array $values, $data): \Closure + { + return function (int|string|null $value): string { + if ('_header' === $value) { + return 'Participant role'; + } + + if (null === $value || '' === $value || null === $r = $this->roleRepository->find($value)) { + return ''; + } + + return $this->translatableStringHelper->localize($r->getName()); + }; + } + + public function getQueryKeys($data): array + { + return [self::KEY]; + } + + public function getTitle() + { + return 'Group by participant role'; + } +} diff --git a/src/Bundle/ChillEventBundle/Export/Declarations.php b/src/Bundle/ChillEventBundle/Export/Declarations.php new file mode 100644 index 000000000..8a873a673 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Export/Declarations.php @@ -0,0 +1,22 @@ +filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center']; + } + + public function buildForm(FormBuilderInterface $builder) + { + } + + public function getFormDefaultData(): array + { + return []; + } + + public function getAllowedFormattersTypes() + { + return [FormatterInterface::TYPE_TABULAR]; + } + + public function getDescription() + { + return 'Count participants to an event by various parameters.'; + } + + public function getGroup(): string + { + return 'Exports of events'; + } + + public function getLabels($key, array $values, $data) + { + if ('export_count_event_participants' !== $key) { + throw new \LogicException("the key {$key} is not used by this export"); + } + + return static fn ($value) => '_header' === $value ? 'Count event participants' : $value; + } + + public function getQueryKeys($data) + { + return ['export_count_event_participants']; + } + + public function getResult($query, $data) + { + return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); + } + + public function getTitle() + { + return 'Count event participants'; + } + + public function getType(): string + { + return Declarations::EVENT_PARTICIPANTS; + } + + public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + { + $centers = array_map(static fn ($el) => $el['center'], $acl); + + $qb = $this->participationRepository + ->createQueryBuilder('event_part') + ->join('event_part.person', 'person'); + + $qb->select('COUNT(event_part.id) as export_count_event_participants'); + + if ($this->filterStatsByCenters) { + $qb + ->andWhere( + $qb->expr()->exists( + 'SELECT 1 FROM '.PersonCenterHistory::class.' acl_count_person_history WHERE acl_count_person_history.person = person + AND acl_count_person_history.center IN (:authorized_centers) + ' + ) + ) + ->setParameter('authorized_centers', $centers); + } + + return $qb; + } + + public function requiredRole(): string + { + return ParticipationVoter::STATS; + } + + public function supportsModifiers() + { + return [ + Declarations::EVENT_PARTICIPANTS, + PersonDeclarations::PERSON_TYPE, + ]; + } +} diff --git a/src/Bundle/ChillEventBundle/Export/Export/CountEvents.php b/src/Bundle/ChillEventBundle/Export/Export/CountEvents.php new file mode 100644 index 000000000..5a1930862 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Export/Export/CountEvents.php @@ -0,0 +1,128 @@ +filterStatsByCenters = $parameterBag->get('chill_main')['acl']['filter_stats_by_center']; + } + + public function buildForm(FormBuilderInterface $builder) + { + } + + public function getFormDefaultData(): array + { + return []; + } + + public function getAllowedFormattersTypes() + { + return [FormatterInterface::TYPE_TABULAR]; + } + + public function getDescription() + { + return 'Count events by various parameters.'; + } + + public function getGroup(): string + { + return 'Exports of events'; + } + + public function getLabels($key, array $values, $data) + { + if ('export_count_event' !== $key) { + throw new \LogicException("the key {$key} is not used by this export"); + } + + return static fn ($value) => '_header' === $value ? 'Number of events' : $value; + } + + public function getQueryKeys($data) + { + return ['export_count_event']; + } + + public function getResult($query, $data) + { + return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); + } + + public function getTitle() + { + return 'Count events'; + } + + public function getType(): string + { + return Declarations::EVENT; + } + + public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + { + $centers = array_map(static fn ($el) => $el['center'], $acl); + + $qb = $this->eventRepository + ->createQueryBuilder('event') + ->leftJoin('event.participations', 'epart') + ->leftJoin('epart.person', 'person'); + + $qb->select('COUNT(event.id) as export_count_event'); + + if ($this->filterStatsByCenters) { + $qb + ->andWhere( + $qb->expr()->exists( + 'SELECT 1 FROM '.PersonCenterHistory::class.' acl_count_person_history WHERE acl_count_person_history.person = person + AND acl_count_person_history.center IN (:authorized_centers) + ' + ) + ) + ->setParameter('authorized_centers', $centers); + } + + return $qb; + } + + public function requiredRole(): string + { + return EventVoter::STATS; + } + + public function supportsModifiers() + { + return [ + Declarations::EVENT, + PersonDeclarations::PERSON_TYPE, + ]; + } +} diff --git a/src/Bundle/ChillEventBundle/Export/Filter/EventDateFilter.php b/src/Bundle/ChillEventBundle/Export/Filter/EventDateFilter.php new file mode 100644 index 000000000..650c5348d --- /dev/null +++ b/src/Bundle/ChillEventBundle/Export/Filter/EventDateFilter.php @@ -0,0 +1,97 @@ +getDQLPart('where'); + $clause = $qb->expr()->between( + 'event.date', + ':date_from', + ':date_to' + ); + + if ($where instanceof Expr\Andx) { + $where->add($clause); + } else { + $where = $qb->expr()->andX($clause); + } + + $qb->add('where', $where); + $qb->setParameter( + 'date_from', + $this->rollingDateConverter->convert($data['date_from']) + ); + $qb->setParameter( + 'date_to', + $this->rollingDateConverter->convert($data['date_to']) + ); + } + + public function applyOn(): string + { + return Declarations::EVENT; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder + ->add('date_from', PickRollingDateType::class, [ + 'label' => 'Events after this date', + ]) + ->add('date_to', PickRollingDateType::class, [ + 'label' => 'Events before this date', + ]); + } + + public function getFormDefaultData(): array + { + return ['date_from' => new RollingDate(RollingDate::T_YEAR_PREVIOUS_START), 'date_to' => new RollingDate(RollingDate::T_TODAY)]; + } + + public function describeAction($data, $format = 'string') + { + return [ + 'Filtered by date of event: only between %date_from% and %date_to%', + [ + '%date_from%' => $this->rollingDateConverter->convert($data['date_from'])->format('d-m-Y'), + '%date_to%' => $this->rollingDateConverter->convert($data['date_to'])->format('d-m-Y'), + ], + ]; + } + + public function getTitle() + { + return 'Filtered by event date'; + } +} diff --git a/src/Bundle/ChillEventBundle/Export/Filter/EventTypeFilter.php b/src/Bundle/ChillEventBundle/Export/Filter/EventTypeFilter.php new file mode 100644 index 000000000..d4c1a96c0 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Export/Filter/EventTypeFilter.php @@ -0,0 +1,95 @@ +expr()->in('event.type', ':selected_event_types'); + + $qb->andWhere($clause); + $qb->setParameter('selected_event_types', $data['types']); + } + + public function applyOn(): string + { + return Declarations::EVENT; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('types', EntityType::class, [ + 'choices' => $this->eventTypeRepository->findAllActive(), + 'class' => EventType::class, + 'choice_label' => fn (EventType $ety) => $this->translatableStringHelper->localize($ety->getName()), + 'multiple' => true, + 'expanded' => false, + 'attr' => [ + 'class' => 'select2', + ], + ]); + } + + public function getFormDefaultData(): array + { + return []; + } + + public function describeAction($data, $format = 'string') + { + $typeNames = array_map( + fn (EventType $t): string => $this->translatableStringHelper->localize($t->getName()), + $this->eventTypeRepository->findBy(['id' => $data['types'] instanceof \Doctrine\Common\Collections\Collection ? $data['types']->toArray() : $data['types']]) + ); + + return ['Filtered by event type: only %list%', [ + '%list%' => implode(', ', $typeNames), + ]]; + } + + public function getTitle() + { + return 'Filtered by event type'; + } + + public function validateForm($data, ExecutionContextInterface $context) + { + if (null === $data['types'] || 0 === \count($data['types'])) { + $context + ->buildViolation('At least one type must be chosen') + ->addViolation(); + } + } +} diff --git a/src/Bundle/ChillEventBundle/Export/Filter/RoleFilter.php b/src/Bundle/ChillEventBundle/Export/Filter/RoleFilter.php new file mode 100644 index 000000000..d791bafe7 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Export/Filter/RoleFilter.php @@ -0,0 +1,95 @@ +expr()->in('event_part.role', ':selected_part_roles'); + + $qb->andWhere($clause); + $qb->setParameter('selected_part_roles', $data['part_roles']); + } + + public function applyOn(): string + { + return Declarations::EVENT_PARTICIPANTS; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('part_roles', EntityType::class, [ + 'choices' => $this->roleRepository->findAllActive(), + 'class' => Role::class, + 'choice_label' => fn (Role $r) => $this->translatableStringHelper->localize($r->getName()), + 'multiple' => true, + 'expanded' => false, + 'attr' => [ + 'class' => 'select2', + ], + ]); + } + + public function getFormDefaultData(): array + { + return []; + } + + public function describeAction($data, $format = 'string') + { + $roleNames = array_map( + fn (Role $r): string => $this->translatableStringHelper->localize($r->getName()), + $this->roleRepository->findBy(['id' => $data['part_roles'] instanceof \Doctrine\Common\Collections\Collection ? $data['part_roles']->toArray() : $data['part_roles']]) + ); + + return ['Filtered by participant roles: only %list%', [ + '%list%' => implode(', ', $roleNames), + ]]; + } + + public function getTitle() + { + return 'Filter by participant roles'; + } + + public function validateForm($data, ExecutionContextInterface $context) + { + if (null === $data['part_roles'] || 0 === \count($data['part_roles'])) { + $context + ->buildViolation('At least one role must be chosen') + ->addViolation(); + } + } +} diff --git a/src/Bundle/ChillEventBundle/Menu/PersonMenuBuilder.php b/src/Bundle/ChillEventBundle/Menu/PersonMenuBuilder.php index 8abfc7cd6..e14e4c979 100644 --- a/src/Bundle/ChillEventBundle/Menu/PersonMenuBuilder.php +++ b/src/Bundle/ChillEventBundle/Menu/PersonMenuBuilder.php @@ -11,7 +11,7 @@ declare(strict_types=1); namespace Chill\EventBundle\Menu; -use Chill\EventBundle\Security\Authorization\EventVoter; +use Chill\EventBundle\Security\EventVoter; use Chill\MainBundle\Routing\LocalMenuBuilderInterface; use Knp\Menu\MenuItem; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; diff --git a/src/Bundle/ChillEventBundle/Menu/SectionMenuBuilder.php b/src/Bundle/ChillEventBundle/Menu/SectionMenuBuilder.php index 2c78392ee..072f6e6bd 100644 --- a/src/Bundle/ChillEventBundle/Menu/SectionMenuBuilder.php +++ b/src/Bundle/ChillEventBundle/Menu/SectionMenuBuilder.php @@ -11,7 +11,7 @@ declare(strict_types=1); namespace Chill\EventBundle\Menu; -use Chill\EventBundle\Security\Authorization\EventVoter; +use Chill\EventBundle\Security\EventVoter; use Chill\MainBundle\Routing\LocalMenuBuilderInterface; use Knp\Menu\MenuItem; use Symfony\Component\Security\Core\Security; diff --git a/src/Bundle/ChillEventBundle/Repository/EventACLAwareRepository.php b/src/Bundle/ChillEventBundle/Repository/EventACLAwareRepository.php index f92b1e825..7520d68e7 100644 --- a/src/Bundle/ChillEventBundle/Repository/EventACLAwareRepository.php +++ b/src/Bundle/ChillEventBundle/Repository/EventACLAwareRepository.php @@ -13,7 +13,7 @@ namespace Chill\EventBundle\Repository; use Chill\EventBundle\Entity\Event; use Chill\EventBundle\Entity\Participation; -use Chill\EventBundle\Security\Authorization\EventVoter; +use Chill\EventBundle\Security\EventVoter; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface; use Chill\PersonBundle\Entity\Person; diff --git a/src/Bundle/ChillEventBundle/Repository/RoleRepository.php b/src/Bundle/ChillEventBundle/Repository/RoleRepository.php index fa0524a6d..b5444a85b 100644 --- a/src/Bundle/ChillEventBundle/Repository/RoleRepository.php +++ b/src/Bundle/ChillEventBundle/Repository/RoleRepository.php @@ -12,13 +12,57 @@ declare(strict_types=1); namespace Chill\EventBundle\Repository; use Chill\EventBundle\Entity\Role; -use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; -use Doctrine\Persistence\ManagerRegistry; +use Chill\MainBundle\Templating\TranslatableStringHelper; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\QueryBuilder; +use Doctrine\Persistence\ObjectRepository; -class RoleRepository extends ServiceEntityRepository +readonly class RoleRepository implements ObjectRepository { - public function __construct(ManagerRegistry $registry) + private EntityRepository $repository; + + public function __construct(EntityManagerInterface $entityManager, private TranslatableStringHelper $translatableStringHelper) { - parent::__construct($registry, Role::class); + $this->repository = $entityManager->getRepository(Role::class); + } + + public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder + { + return $this->repository->createQueryBuilder($alias, $indexBy); + } + + public function find($id) + { + return $this->repository->find($id); + } + + public function findAll(): array + { + return $this->repository->findAll(); + } + + public function findAllActive(): array + { + $roles = $this->repository->findBy(['active' => true]); + + usort($roles, fn (Role $a, Role $b) => $this->translatableStringHelper->localize($a->getName()) <=> $this->translatableStringHelper->localize($b->getName())); + + return $roles; + } + + public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array + { + return $this->repository->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria) + { + return $this->repository->findOneBy($criteria); + } + + public function getClassName(): string + { + return Role::class; } } diff --git a/src/Bundle/ChillEventBundle/Security/Authorization/EventVoter.php b/src/Bundle/ChillEventBundle/Security/EventVoter.php similarity index 60% rename from src/Bundle/ChillEventBundle/Security/Authorization/EventVoter.php rename to src/Bundle/ChillEventBundle/Security/EventVoter.php index c88f2804a..e490e0518 100644 --- a/src/Bundle/ChillEventBundle/Security/Authorization/EventVoter.php +++ b/src/Bundle/ChillEventBundle/Security/EventVoter.php @@ -9,18 +9,19 @@ declare(strict_types=1); * the LICENSE file that was distributed with this source code. */ -namespace Chill\EventBundle\Security\Authorization; +namespace Chill\EventBundle\Security; use Chill\EventBundle\Entity\Event; +use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Security\Authorization\AbstractChillVoter; use Chill\MainBundle\Security\Authorization\AuthorizationHelper; +use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface; +use Chill\MainBundle\Security\Authorization\VoterHelperInterface; use Chill\MainBundle\Security\ProvideRoleHierarchyInterface; use Chill\PersonBundle\Entity\Person; -use Chill\PersonBundle\Security\Authorization\PersonVoter; use Psr\Log\LoggerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; /** * Description of EventVoter. @@ -42,61 +43,46 @@ class EventVoter extends AbstractChillVoter implements ProvideRoleHierarchyInter final public const UPDATE = 'CHILL_EVENT_UPDATE'; - /** - * @var AccessDecisionManagerInterface - */ - protected $accessDecisionManager; + final public const STATS = 'CHILL_EVENT_STATS'; - /** - * @var AuthorizationHelper - */ - protected $authorizationHelper; - - /** - * @var LoggerInterface - */ - protected $logger; + private readonly VoterHelperInterface $voterHelper; public function __construct( - AccessDecisionManagerInterface $accessDecisionManager, - AuthorizationHelper $authorizationHelper, - LoggerInterface $logger + private readonly AuthorizationHelper $authorizationHelper, + private readonly LoggerInterface $logger, + VoterHelperFactoryInterface $voterHelperFactory ) { - $this->accessDecisionManager = $accessDecisionManager; - $this->authorizationHelper = $authorizationHelper; - $this->logger = $logger; + $this->voterHelper = $voterHelperFactory + ->generate(self::class) + ->addCheckFor(null, [self::SEE]) + ->addCheckFor(Event::class, [...self::ROLES]) + ->addCheckFor(Person::class, [self::SEE, self::CREATE]) + ->addCheckFor(Center::class, [self::STATS]) + ->build(); } public function getRoles(): array { - return self::ROLES; + return [...self::ROLES, self::STATS]; } public function getRolesWithHierarchy(): array { return [ - 'Event' => self::ROLES, + 'Event' => $this->getRoles(), ]; } public function getRolesWithoutScope(): array { - return []; + return [self::ROLES, self::STATS]; } public function supports($attribute, $subject) { - return ($subject instanceof Event && \in_array($attribute, self::ROLES, true)) - || ($subject instanceof Person && \in_array($attribute, [self::CREATE, self::SEE], true)) - || (null === $subject && self::SEE === $attribute); + return $this->voterHelper->supports($attribute, $subject); } - /** - * @param string $attribute - * @param Event $subject - * - * @return bool - */ protected function voteOnAttribute($attribute, $subject, TokenInterface $token) { $this->logger->debug(sprintf('Voting from %s class', self::class)); @@ -118,15 +104,5 @@ class EventVoter extends AbstractChillVoter implements ProvideRoleHierarchyInter ->getReachableCenters($token->getUser(), $attribute); return \count($centers) > 0; - - if (!$this->accessDecisionManager->decide($token, [PersonVoter::SEE], $person)) { - return false; - } - - return $this->authorizationHelper->userHasAccess( - $token->getUser(), - $subject, - $attribute - ); } } diff --git a/src/Bundle/ChillEventBundle/Security/Authorization/ParticipationVoter.php b/src/Bundle/ChillEventBundle/Security/ParticipationVoter.php similarity index 62% rename from src/Bundle/ChillEventBundle/Security/Authorization/ParticipationVoter.php rename to src/Bundle/ChillEventBundle/Security/ParticipationVoter.php index ad2e90377..670cff815 100644 --- a/src/Bundle/ChillEventBundle/Security/Authorization/ParticipationVoter.php +++ b/src/Bundle/ChillEventBundle/Security/ParticipationVoter.php @@ -9,18 +9,19 @@ declare(strict_types=1); * the LICENSE file that was distributed with this source code. */ -namespace Chill\EventBundle\Security\Authorization; +namespace Chill\EventBundle\Security; use Chill\EventBundle\Entity\Participation; +use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Security\Authorization\AbstractChillVoter; use Chill\MainBundle\Security\Authorization\AuthorizationHelper; +use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface; +use Chill\MainBundle\Security\Authorization\VoterHelperInterface; use Chill\MainBundle\Security\ProvideRoleHierarchyInterface; use Chill\PersonBundle\Entity\Person; -use Chill\PersonBundle\Security\Authorization\PersonVoter; use Psr\Log\LoggerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; class ParticipationVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface { @@ -39,40 +40,33 @@ class ParticipationVoter extends AbstractChillVoter implements ProvideRoleHierar final public const UPDATE = 'CHILL_EVENT_PARTICIPATION_UPDATE'; - /** - * @var AccessDecisionManagerInterface - */ - protected $accessDecisionManager; + final public const STATS = 'CHILL_EVENT_PARTICIPATION_STATS'; - /** - * @var AuthorizationHelper - */ - protected $authorizationHelper; - - /** - * @var LoggerInterface - */ - protected $logger; + private readonly VoterHelperInterface $voterHelper; public function __construct( - AccessDecisionManagerInterface $accessDecisionManager, - AuthorizationHelper $authorizationHelper, - LoggerInterface $logger + private readonly AuthorizationHelper $authorizationHelper, + private readonly LoggerInterface $logger, + VoterHelperFactoryInterface $voterHelperFactory ) { - $this->accessDecisionManager = $accessDecisionManager; - $this->authorizationHelper = $authorizationHelper; - $this->logger = $logger; + $this->voterHelper = $voterHelperFactory + ->generate(self::class) + ->addCheckFor(null, [self::SEE]) + ->addCheckFor(Participation::class, [...self::ROLES]) + ->addCheckFor(Person::class, [self::SEE, self::CREATE]) + ->addCheckFor(Center::class, [self::STATS]) + ->build(); } public function getRoles(): array { - return self::ROLES; + return [...self::ROLES, self::STATS]; } public function getRolesWithHierarchy(): array { return [ - 'Event' => self::ROLES, + 'Participation' => $this->getRoles(), ]; } @@ -83,14 +77,11 @@ class ParticipationVoter extends AbstractChillVoter implements ProvideRoleHierar public function supports($attribute, $subject) { - return ($subject instanceof Participation && \in_array($attribute, self::ROLES, true)) - || ($subject instanceof Person && \in_array($attribute, [self::CREATE, self::SEE], true)) - || (null === $subject && self::SEE === $attribute); + return $this->voterHelper->supports($attribute, $subject); } /** - * @param string $attribute - * @param Participation $subject + * @param string $attribute * * @return bool */ @@ -115,15 +106,5 @@ class ParticipationVoter extends AbstractChillVoter implements ProvideRoleHierar ->getReachableCenters($token->getUser(), $attribute); return \count($centers) > 0; - - if (!$this->accessDecisionManager->decide($token, [PersonVoter::SEE], $person)) { - return false; - } - - return $this->authorizationHelper->userHasAccess( - $token->getUser(), - $subject, - $attribute - ); } } diff --git a/src/Bundle/ChillEventBundle/Tests/Export/CountEventParticipationsTest.php b/src/Bundle/ChillEventBundle/Tests/Export/CountEventParticipationsTest.php new file mode 100644 index 000000000..3d606dd04 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Tests/Export/CountEventParticipationsTest.php @@ -0,0 +1,43 @@ +countEventParticipations = self::$container->get(CountEventParticipations::class); + } + + public function testExecuteQuery(): void + { + $qb = $this->countEventParticipations->initiateQuery([], [], []) + ->setMaxResults(1); + + $results = $qb->getQuery()->getResult(AbstractQuery::HYDRATE_ARRAY); + + self::assertIsArray($results, 'smoke test: test that the result is an array'); + } +} diff --git a/src/Bundle/ChillEventBundle/Tests/Export/CountEventTest.php b/src/Bundle/ChillEventBundle/Tests/Export/CountEventTest.php new file mode 100644 index 000000000..4afbe64e9 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Tests/Export/CountEventTest.php @@ -0,0 +1,43 @@ +countEvents = self::$container->get(CountEvents::class); + } + + public function testExecuteQuery(): void + { + $qb = $this->countEvents->initiateQuery([], [], []) + ->setMaxResults(1); + + $results = $qb->getQuery()->getResult(AbstractQuery::HYDRATE_ARRAY); + + self::assertIsArray($results, 'smoke test: test that the result is an array'); + } +} diff --git a/src/Bundle/ChillEventBundle/Tests/Export/aggregators/EventDateAggregatorTest.php b/src/Bundle/ChillEventBundle/Tests/Export/aggregators/EventDateAggregatorTest.php new file mode 100644 index 000000000..96e377f4e --- /dev/null +++ b/src/Bundle/ChillEventBundle/Tests/Export/aggregators/EventDateAggregatorTest.php @@ -0,0 +1,59 @@ +aggregator = self::$container->get(EventDateAggregator::class); + } + + public function getAggregator() + { + return $this->aggregator; + } + + public function getFormData(): array|\Generator + { + yield ['frequency' => 'YYYY']; + yield ['frequency' => 'YYYY-MM']; + yield ['frequency' => 'YYYY-IV']; + } + + public function getQueryBuilders(): array + { + self::bootKernel(); + + $em = self::$container->get(EntityManagerInterface::class); + + return [ + $em->createQueryBuilder() + ->select('event.id') + ->from(Event::class, 'event'), + ]; + } +} diff --git a/src/Bundle/ChillEventBundle/Tests/Export/aggregators/EventTypeAggregatorTest.php b/src/Bundle/ChillEventBundle/Tests/Export/aggregators/EventTypeAggregatorTest.php new file mode 100644 index 000000000..321eae153 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Tests/Export/aggregators/EventTypeAggregatorTest.php @@ -0,0 +1,59 @@ +aggregator = self::$container->get(EventTypeAggregator::class); + } + + public function getAggregator() + { + return $this->aggregator; + } + + public function getFormData(): array + { + return [ + [], + ]; + } + + public function getQueryBuilders(): array + { + self::bootKernel(); + + $em = self::$container->get(EntityManagerInterface::class); + + return [ + $em->createQueryBuilder() + ->select('event.id') + ->from(Event::class, 'event'), + ]; + } +} diff --git a/src/Bundle/ChillEventBundle/Tests/Export/aggregators/RoleAggregatorTest.php b/src/Bundle/ChillEventBundle/Tests/Export/aggregators/RoleAggregatorTest.php new file mode 100644 index 000000000..0e4f0a896 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Tests/Export/aggregators/RoleAggregatorTest.php @@ -0,0 +1,63 @@ +aggregator = self::$container->get(RoleAggregator::class); + } + + public function getAggregator() + { + return $this->aggregator; + } + + public function getFormData(): array + { + return [ + [], + ]; + } + + public function getQueryBuilders(): array + { + self::bootKernel(); + + $em = self::$container->get(EntityManagerInterface::class); + + return [ + $em->createQueryBuilder() + ->select('event.id') + ->from(Event::class, 'event'), + $em->createQueryBuilder() + ->select('event_part') + ->from(Participation::class, 'event_part'), + ]; + } +} diff --git a/src/Bundle/ChillEventBundle/Tests/Export/filters/EventDateFilterTest.php b/src/Bundle/ChillEventBundle/Tests/Export/filters/EventDateFilterTest.php new file mode 100644 index 000000000..dd13c5c77 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Tests/Export/filters/EventDateFilterTest.php @@ -0,0 +1,65 @@ +rollingDateConverter = self::$container->get(RollingDateConverterInterface::class); + } + + public function getFilter() + { + return new EventDateFilter($this->rollingDateConverter); + } + + public function getFormData() + { + return [ + [ + 'date_from' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), + 'date_to' => new RollingDate(RollingDate::T_TODAY), + ], + ]; + } + + public function getQueryBuilders(): array + { + self::bootKernel(); + + $em = self::$container->get(EntityManagerInterface::class); + + return [ + $em->createQueryBuilder() + ->select('event.id') + ->from(Event::class, 'event'), + ]; + } +} diff --git a/src/Bundle/ChillEventBundle/Tests/Export/filters/EventTypeFilterTest.php b/src/Bundle/ChillEventBundle/Tests/Export/filters/EventTypeFilterTest.php new file mode 100644 index 000000000..8d54c8068 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Tests/Export/filters/EventTypeFilterTest.php @@ -0,0 +1,76 @@ +filter = self::$container->get(EventTypeFilter::class); + } + + public function getFilter(): EventTypeFilter|\Chill\MainBundle\Export\FilterInterface + { + return $this->filter; + } + + public function getFormData() + { + self::bootKernel(); + + $em = self::$container->get(EntityManagerInterface::class); + + $array = $em->createQueryBuilder() + ->from(EventType::class, 'et') + ->select('et') + ->getQuery() + ->getResult(); + + $data = []; + + foreach ($array as $a) { + $data[] = [ + 'types' => new ArrayCollection([$a]), + ]; + } + + return $data; + } + + public function getQueryBuilders() + { + self::bootKernel(); + + $em = self::$container->get(EntityManagerInterface::class); + + return [ + $em->createQueryBuilder() + ->select('event.id') + ->from(Event::class, 'event'), + ]; + } +} diff --git a/src/Bundle/ChillEventBundle/Tests/Export/filters/RoleFilterTest.php b/src/Bundle/ChillEventBundle/Tests/Export/filters/RoleFilterTest.php new file mode 100644 index 000000000..f03aac5e0 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Tests/Export/filters/RoleFilterTest.php @@ -0,0 +1,81 @@ +filter = self::$container->get(RoleFilter::class); + } + + public function getFilter() + { + return $this->filter; + } + + public function getFormData(): array + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + $array = $em->createQueryBuilder() + ->from(Role::class, 'r') + ->select('r') + ->getQuery() + ->setMaxResults(1) + ->getResult(); + + $data = []; + + foreach ($array as $a) { + $data[] = [ + 'roles' => new ArrayCollection([$a]), + ]; + } + + return $data; + } + + public function getQueryBuilders() + { + self::bootKernel(); + + $em = self::$container->get(EntityManagerInterface::class); + + return [ + $em->createQueryBuilder() + ->select('event.id') + ->from(Event::class, 'event'), + $em->createQueryBuilder() + ->select('event_part') + ->from(Participation::class, 'event_part'), + ]; + } +} diff --git a/src/Bundle/ChillEventBundle/Tests/Repository/EventACLAwareRepositoryTest.php b/src/Bundle/ChillEventBundle/Tests/Repository/EventACLAwareRepositoryTest.php index f489f6cd4..c830c2613 100644 --- a/src/Bundle/ChillEventBundle/Tests/Repository/EventACLAwareRepositoryTest.php +++ b/src/Bundle/ChillEventBundle/Tests/Repository/EventACLAwareRepositoryTest.php @@ -12,7 +12,7 @@ declare(strict_types=1); namespace Chill\EventBundle\Tests\Repository; use Chill\EventBundle\Repository\EventACLAwareRepository; -use Chill\EventBundle\Security\Authorization\EventVoter; +use Chill\EventBundle\Security\EventVoter; use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User; diff --git a/src/Bundle/ChillEventBundle/config/services/authorization.yaml b/src/Bundle/ChillEventBundle/config/services/authorization.yaml deleted file mode 100644 index ca1eb789b..000000000 --- a/src/Bundle/ChillEventBundle/config/services/authorization.yaml +++ /dev/null @@ -1,20 +0,0 @@ -services: - chill_event.event_voter: - class: Chill\EventBundle\Security\Authorization\EventVoter - arguments: - - "@security.access.decision_manager" - - "@chill.main.security.authorization.helper" - - "@logger" - tags: - - { name: chill.role } - - { name: security.voter } - - chill_event.event_participation: - class: Chill\EventBundle\Security\Authorization\ParticipationVoter - arguments: - - "@security.access.decision_manager" - - "@chill.main.security.authorization.helper" - - "@logger" - tags: - - { name: chill.role } - - { name: security.voter } diff --git a/src/Bundle/ChillEventBundle/config/services/export.yaml b/src/Bundle/ChillEventBundle/config/services/export.yaml new file mode 100644 index 000000000..8f8399e31 --- /dev/null +++ b/src/Bundle/ChillEventBundle/config/services/export.yaml @@ -0,0 +1,41 @@ +services: + _defaults: + autowire: true + autoconfigure: true + + # indicators + + Chill\EventBundle\Export\Export\CountEvents: + tags: + - { name: chill.export, alias: 'count_events' } + Chill\EventBundle\Export\Export\CountEventParticipations: + tags: + - { name: chill.export, alias: 'count_event_participants' } + + # filters + + Chill\EventBundle\Export\Filter\EventDateFilter: + tags: + - { name: chill.export_filter, alias: 'event_date_filter' } + + Chill\EventBundle\Export\Filter\EventTypeFilter: + tags: + - { name: chill.export_filter, alias: 'event_type_filter' } + + Chill\EventBundle\Export\Filter\RoleFilter: + tags: + - { name: chill.export_filter, alias: 'role_filter' } + + # aggregators + + Chill\EventBundle\Export\Aggregator\EventTypeAggregator: + tags: + - { name: chill.export_aggregator, alias: event_type_aggregator } + + Chill\EventBundle\Export\Aggregator\EventDateAggregator: + tags: + - { name: chill.export_aggregator, alias: event_date_aggregator } + + Chill\EventBundle\Export\Aggregator\RoleAggregator: + tags: + - { name: chill.export_aggregator, alias: role_aggregator } diff --git a/src/Bundle/ChillEventBundle/config/services/security.yaml b/src/Bundle/ChillEventBundle/config/services/security.yaml new file mode 100644 index 000000000..13198d55f --- /dev/null +++ b/src/Bundle/ChillEventBundle/config/services/security.yaml @@ -0,0 +1,14 @@ +services: + Chill\EventBundle\Security\EventVoter: + autowire: true + autoconfigure: true + tags: + - { name: security.voter } + - { name: chill.role } + + Chill\EventBundle\Security\ParticipationVoter: + autowire: true + autoconfigure: true + tags: + - { name: security.voter } + - { name: chill.role } diff --git a/src/Bundle/ChillEventBundle/translations/messages.fr.yml b/src/Bundle/ChillEventBundle/translations/messages.fr.yml index 60d888f07..bb9f8acd3 100644 --- a/src/Bundle/ChillEventBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillEventBundle/translations/messages.fr.yml @@ -81,9 +81,31 @@ Pick an event: Choisir un événement Pick a type of event: Choisir un type d'événement Pick a moderator: Choisir un animateur +# exports Select a format: Choisir un format Export: Exporter +Count events: Nombre d'événements +Count events by various parameters.: Compte le nombre d'événements selon divers critères +Exports of events: Exports d'événements + +Filtered by event date: Filtrer par date d'événement +'Filtered by date of event: only between %date_from% and %date_to%': "Filtré par date d'événement: uniquement entre le %date_from% et le %date_to%" +Events after this date: Événements après cette date +Events before this date: Événements avant cette date +Filtered by event type: Filtrer par type d'événement +'Filtered by event type: only %list%': "Filtré par type: uniquement %list%" +Group event by date: Grouper par date d'événement +Group by event type: Grouper par type d'événement + +Count event participants: Nombre de participations +Count participants to an event by various parameters.: Compte le nombre de participations selon divers critères +Exports of event participants: Exports de participations +'Filtered by participant roles: only %list%': "Filtré par rôles de participation: uniquement %list%" +Filter by participant roles: Filtrer par rôles de participation +Part roles: Rôles de participation +Group by participant role: Grouper par rôle de participation + Events configuration: Configuration des événements Events configuration menu: Menu des événements From 145419a76b18af360b9634bd2e800568bfe4fda2 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 25 Jun 2024 13:29:24 +0200 Subject: [PATCH 02/13] CHANGELOG entry added for exports in event bundle --- .changes/v2.22.0.md | 6 ++++++ CHANGELOG.md | 7 +++++++ 2 files changed, 13 insertions(+) create mode 100644 .changes/v2.22.0.md diff --git a/.changes/v2.22.0.md b/.changes/v2.22.0.md new file mode 100644 index 000000000..fef006fd0 --- /dev/null +++ b/.changes/v2.22.0.md @@ -0,0 +1,6 @@ +## v2.22.0 - 2024-06-25 +### Feature +* ([#216](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/216)) [event bundle] exports added for the event module + +### Traduction francophone +* Exports sont ajoutés pour la module événement. diff --git a/CHANGELOG.md b/CHANGELOG.md index dc8f2305a..47b3760b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). +## v2.22.0 - 2024-06-25 +### Feature +* ([#216](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/216)) [event bundle] exports added for the event module + +### Traduction francophone +* Exports sont ajoutés pour la module événement. + ## v2.21.0 - 2024-06-18 ### Feature * Add flash menu buttons in search results, to open directly a new calendar, or a new activity in an accompanying period From ba25c181f5c228419af4811ce9d0bdb7f5c39be7 Mon Sep 17 00:00:00 2001 From: nobohan Date: Thu, 27 Jun 2024 10:07:24 +0200 Subject: [PATCH 03/13] DX import Luxembourg address command --- .changes/unreleased/DX-20240627-100653.yaml | 6 ++ .../LoadAddressesLUFromBDAddressCommand.php | 40 +++++++ .../Service/Import/AddressReferenceLU.php | 102 ++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 .changes/unreleased/DX-20240627-100653.yaml create mode 100644 src/Bundle/ChillMainBundle/Command/LoadAddressesLUFromBDAddressCommand.php create mode 100644 src/Bundle/ChillMainBundle/Service/Import/AddressReferenceLU.php diff --git a/.changes/unreleased/DX-20240627-100653.yaml b/.changes/unreleased/DX-20240627-100653.yaml new file mode 100644 index 000000000..538cf2697 --- /dev/null +++ b/.changes/unreleased/DX-20240627-100653.yaml @@ -0,0 +1,6 @@ +kind: DX +body: Add a command for reading official address DB from Luxembourg and update chill + addresses +time: 2024-06-27T10:06:53.098022939+02:00 +custom: + Issue: "" diff --git a/src/Bundle/ChillMainBundle/Command/LoadAddressesLUFromBDAddressCommand.php b/src/Bundle/ChillMainBundle/Command/LoadAddressesLUFromBDAddressCommand.php new file mode 100644 index 000000000..2477a486d --- /dev/null +++ b/src/Bundle/ChillMainBundle/Command/LoadAddressesLUFromBDAddressCommand.php @@ -0,0 +1,40 @@ +setName('chill:main:address-ref-lux'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->addressImporter->import(); + + return Command::SUCCESS; + } +} diff --git a/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceLU.php b/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceLU.php new file mode 100644 index 000000000..c29921847 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceLU.php @@ -0,0 +1,102 @@ +client->request('GET', $downloadUrl); + + if (200 !== $response->getStatusCode()) { + throw new \Exception('Could not download CSV: '.$response->getStatusCode()); + } + + $tmpname = tempnam(sys_get_temp_dir(), 'php-add-'); + $file = fopen($tmpname, 'r+b'); + + foreach ($this->client->stream($response) as $chunk) { + fwrite($file, $chunk->getContent()); + } + + fclose($file); + + $uncompressedStream = gzopen($tmpname, 'r'); + + $csv = Reader::createFromStream($uncompressedStream); + $csv->setDelimiter(';'); + $csv->setHeaderOffset(0); + + $this->process_postal_code($csv); + + $this->process_address($csv); + + $this->addressToReferenceMatcher->checkAddressesMatchingReferences(); + + gzclose($uncompressedStream); + } + + private function process_address(Reader $csv): void + { + $stmt = Statement::create()->process($csv); + foreach ($stmt as $record) { + $this->addressBaseImporter->importAddress( + $record['id_geoportail'], + $record['code_postal'], + $record['code_postal'], + $record['rue'], + $record['numero'], + 'bd addresses', + (float) $record['lat_wgs84'], + (float) $record['lon_wgs84'], + 4326 + ); + } + + $this->addressBaseImporter->finalize(); + } + + private function process_postal_code(Reader $csv): void + { + $stmt = Statement::create()->process($csv); + $arr_postal_codes = []; + foreach ($stmt as $record) { + if (false === \in_array($record['code_postal'], $arr_postal_codes, true)) { + $this->postalCodeBaseImporter->importCode( + 'LU', + trim((string) $record['localite']), + trim((string) $record['code_postal']), + trim((string) $record['code_postal']), + 'bd addresses', + (float) $record['lat_wgs84'], + (float) $record['lon_wgs84'], + 4326 + ); + \array_push($arr_postal_codes, $record['code_postal']); + } + } + } +} + + + From 25ccb1630899f48895c32e24e271fff996e92694 Mon Sep 17 00:00:00 2001 From: nobohan Date: Thu, 27 Jun 2024 10:17:08 +0200 Subject: [PATCH 04/13] DX import Luxembourg address command - csfixer --- .../ChillMainBundle/Service/Import/AddressReferenceLU.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceLU.php b/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceLU.php index c29921847..3e45fc565 100644 --- a/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceLU.php +++ b/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceLU.php @@ -13,14 +13,15 @@ namespace Chill\MainBundle\Service\Import; use League\Csv\Reader; use League\Csv\Statement; -use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; class AddressReferenceLU { private const RELEASE = 'https://data.public.lu/fr/datasets/r/5cadc5b8-6a7d-4283-87bc-f9e58dd771f7'; - public function __construct(private readonly HttpClientInterface $client, private readonly AddressReferenceBaseImporter $addressBaseImporter, private readonly PostalCodeBaseImporter $postalCodeBaseImporter, private readonly AddressToReferenceMatcher $addressToReferenceMatcher) {} + public function __construct(private readonly HttpClientInterface $client, private readonly AddressReferenceBaseImporter $addressBaseImporter, private readonly PostalCodeBaseImporter $postalCodeBaseImporter, private readonly AddressToReferenceMatcher $addressToReferenceMatcher) + { + } public function import(): void { @@ -97,6 +98,3 @@ class AddressReferenceLU } } } - - - From d9c50cffb7952b43796eef6d9574413921237e84 Mon Sep 17 00:00:00 2001 From: nobohan Date: Thu, 27 Jun 2024 10:34:41 +0200 Subject: [PATCH 05/13] DX import Luxembourg address command - phpstan --- .../Command/LoadAddressesLUFromBDAddressCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillMainBundle/Command/LoadAddressesLUFromBDAddressCommand.php b/src/Bundle/ChillMainBundle/Command/LoadAddressesLUFromBDAddressCommand.php index 2477a486d..499751c7c 100644 --- a/src/Bundle/ChillMainBundle/Command/LoadAddressesLUFromBDAddressCommand.php +++ b/src/Bundle/ChillMainBundle/Command/LoadAddressesLUFromBDAddressCommand.php @@ -35,6 +35,6 @@ class LoadAddressesLUFromBDAddressCommand extends Command { $this->addressImporter->import(); - return Command::SUCCESS; + return 0; } } From c5a24e8ac54135c5c7de24413ddc6ae7646330e0 Mon Sep 17 00:00:00 2001 From: nobohan Date: Fri, 28 Jun 2024 09:27:45 +0200 Subject: [PATCH 06/13] DX: Improve Lux address import command - WIP --- .../Service/Import/AddressReferenceLU.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceLU.php b/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceLU.php index 3e45fc565..f5cb03f19 100644 --- a/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceLU.php +++ b/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceLU.php @@ -42,9 +42,9 @@ class AddressReferenceLU fclose($file); - $uncompressedStream = gzopen($tmpname, 'r'); + //$uncompressedStream = gzopen($tmpname, 'r'); - $csv = Reader::createFromStream($uncompressedStream); + $csv = Reader::createFromStream($file); $csv->setDelimiter(';'); $csv->setHeaderOffset(0); @@ -54,7 +54,7 @@ class AddressReferenceLU $this->addressToReferenceMatcher->checkAddressesMatchingReferences(); - gzclose($uncompressedStream); + //gzclose($uncompressedStream); } private function process_address(Reader $csv): void @@ -67,7 +67,7 @@ class AddressReferenceLU $record['code_postal'], $record['rue'], $record['numero'], - 'bd addresses', + 'bd-addresses.lux', (float) $record['lat_wgs84'], (float) $record['lon_wgs84'], 4326 @@ -82,7 +82,8 @@ class AddressReferenceLU $stmt = Statement::create()->process($csv); $arr_postal_codes = []; foreach ($stmt as $record) { - if (false === \in_array($record['code_postal'], $arr_postal_codes, true)) { + //if (false === \in_array($record['code_postal'], $arr_postal_codes, true)) { + if (false === \array_key_exists($record['code_postal'], $arr_postal_codes, true)) { $this->postalCodeBaseImporter->importCode( 'LU', trim((string) $record['localite']), @@ -93,7 +94,7 @@ class AddressReferenceLU (float) $record['lon_wgs84'], 4326 ); - \array_push($arr_postal_codes, $record['code_postal']); + $arr_postal_codes[$record['code_postal']] = 1; } } } From 9c28df25a1b32499eec56acb4200ac9eb160effe Mon Sep 17 00:00:00 2001 From: nobohan Date: Fri, 28 Jun 2024 10:45:58 +0200 Subject: [PATCH 07/13] DX: Improve Lux adress import command + register in config --- .../Service/Import/AddressReferenceLU.php | 14 +++++--------- .../ChillMainBundle/config/services/command.yaml | 6 ++++++ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceLU.php b/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceLU.php index f5cb03f19..d70b5085d 100644 --- a/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceLU.php +++ b/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceLU.php @@ -33,16 +33,13 @@ class AddressReferenceLU throw new \Exception('Could not download CSV: '.$response->getStatusCode()); } - $tmpname = tempnam(sys_get_temp_dir(), 'php-add-'); - $file = fopen($tmpname, 'r+b'); + $file = tmpfile(); foreach ($this->client->stream($response) as $chunk) { fwrite($file, $chunk->getContent()); } - fclose($file); - - //$uncompressedStream = gzopen($tmpname, 'r'); + fseek($file, 0); $csv = Reader::createFromStream($file); $csv->setDelimiter(';'); @@ -54,7 +51,7 @@ class AddressReferenceLU $this->addressToReferenceMatcher->checkAddressesMatchingReferences(); - //gzclose($uncompressedStream); + fclose($file); } private function process_address(Reader $csv): void @@ -82,14 +79,13 @@ class AddressReferenceLU $stmt = Statement::create()->process($csv); $arr_postal_codes = []; foreach ($stmt as $record) { - //if (false === \in_array($record['code_postal'], $arr_postal_codes, true)) { - if (false === \array_key_exists($record['code_postal'], $arr_postal_codes, true)) { + if (false === \array_key_exists($record['code_postal'], $arr_postal_codes)) { $this->postalCodeBaseImporter->importCode( 'LU', trim((string) $record['localite']), trim((string) $record['code_postal']), trim((string) $record['code_postal']), - 'bd addresses', + 'bd-addresses.lux', (float) $record['lat_wgs84'], (float) $record['lon_wgs84'], 4326 diff --git a/src/Bundle/ChillMainBundle/config/services/command.yaml b/src/Bundle/ChillMainBundle/config/services/command.yaml index 8a2327c0b..94cb2cf97 100644 --- a/src/Bundle/ChillMainBundle/config/services/command.yaml +++ b/src/Bundle/ChillMainBundle/config/services/command.yaml @@ -59,6 +59,12 @@ services: tags: - { name: console.command } + Chill\MainBundle\Command\LoadAddressesLUFromBDAddressCommand: + autoconfigure: true + autowire: true + tags: + - { name: console.command } + Chill\MainBundle\Command\ExecuteCronJobCommand: autoconfigure: true autowire: true From 436661d952a8201337f0665726e942515e63cec5 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 1 Jul 2024 09:19:22 +0200 Subject: [PATCH 08/13] Remove debug word from code --- .../Resources/views/Activity/concernedGroups.html.twig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/concernedGroups.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/concernedGroups.html.twig index 76db92d42..5528ab233 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/concernedGroups.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/concernedGroups.html.twig @@ -87,7 +87,6 @@
  • {% if bloc.type == 'user' %} - hello {{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }} {% else %} From db3961275bd3b91b07c86a1f7892ac8f12a7683f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 1 Jul 2024 09:53:41 +0200 Subject: [PATCH 09/13] git release 2.22.1 --- .changes/unreleased/DX-20240627-100653.yaml | 6 ------ .changes/v2.22.1.md | 5 +++++ 2 files changed, 5 insertions(+), 6 deletions(-) delete mode 100644 .changes/unreleased/DX-20240627-100653.yaml create mode 100644 .changes/v2.22.1.md diff --git a/.changes/unreleased/DX-20240627-100653.yaml b/.changes/unreleased/DX-20240627-100653.yaml deleted file mode 100644 index 538cf2697..000000000 --- a/.changes/unreleased/DX-20240627-100653.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: DX -body: Add a command for reading official address DB from Luxembourg and update chill - addresses -time: 2024-06-27T10:06:53.098022939+02:00 -custom: - Issue: "" diff --git a/.changes/v2.22.1.md b/.changes/v2.22.1.md new file mode 100644 index 000000000..b856cc95b --- /dev/null +++ b/.changes/v2.22.1.md @@ -0,0 +1,5 @@ +## v2.22.1 - 2024-07-01 +### Fixed +* Remove debug word +### DX +* Add a command for reading official address DB from Luxembourg and update chill addresses From 18df08e8c3a03631b986881292dddb5469f9c6ef Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 1 Jul 2024 11:14:02 +0200 Subject: [PATCH 10/13] Do not require scope for event participation stats --- src/Bundle/ChillEventBundle/Security/ParticipationVoter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillEventBundle/Security/ParticipationVoter.php b/src/Bundle/ChillEventBundle/Security/ParticipationVoter.php index 670cff815..368a47cab 100644 --- a/src/Bundle/ChillEventBundle/Security/ParticipationVoter.php +++ b/src/Bundle/ChillEventBundle/Security/ParticipationVoter.php @@ -72,7 +72,7 @@ class ParticipationVoter extends AbstractChillVoter implements ProvideRoleHierar public function getRolesWithoutScope(): array { - return []; + return [self::ROLES, self::STATS]; } public function supports($attribute, $subject) From 3a7ed7ef8fd95909ffc3a2539b19c45e08007e46 Mon Sep 17 00:00:00 2001 From: nobohan Date: Thu, 13 Jun 2024 14:00:25 +0200 Subject: [PATCH 11/13] #271 Account for acp closing date inn action filters (export) --- .changes/unreleased/Fixed-20240613-135945.yaml | 5 +++++ .../AccompanyingPeriodWorkEndDateBetweenDateFilter.php | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changes/unreleased/Fixed-20240613-135945.yaml diff --git a/.changes/unreleased/Fixed-20240613-135945.yaml b/.changes/unreleased/Fixed-20240613-135945.yaml new file mode 100644 index 000000000..38028a735 --- /dev/null +++ b/.changes/unreleased/Fixed-20240613-135945.yaml @@ -0,0 +1,5 @@ +kind: Fixed +body: Take into account the acp closing date in the acp works date filter +time: 2024-06-13T13:59:45.561891547+02:00 +custom: + Issue: "271" diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkEndDateBetweenDateFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkEndDateBetweenDateFilter.php index 92258ccf9..38ca39320 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkEndDateBetweenDateFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkEndDateBetweenDateFilter.php @@ -86,11 +86,11 @@ final readonly class AccompanyingPeriodWorkEndDateBetweenDateFilter implements F }; $end = match ($data['keep_null']) { true => $qb->expr()->orX( - $qb->expr()->gt('acpw.endDate', ':'.$as), + $qb->expr()->gt('COALESCE(acp.closingDate, acpw.endDate)', ':'.$as), $qb->expr()->isNull('acpw.endDate') ), false => $qb->expr()->andX( - $qb->expr()->gt('acpw.endDate', ':'.$as), + $qb->expr()->gt('COALESCE(acp.closingDate, acpw.endDate)', ':'.$as), $qb->expr()->isNotNull('acpw.endDate') ), default => throw new \LogicException('This value is not supported'), From 41dd4d89f7e3535a2f4fa817b278eb0e932163fd Mon Sep 17 00:00:00 2001 From: nobohan Date: Tue, 2 Jul 2024 16:24:45 +0200 Subject: [PATCH 12/13] Revert "#271 Account for acp closing date inn action filters (export)" This reverts commit 3a7ed7ef8fd95909ffc3a2539b19c45e08007e46. --- .changes/unreleased/Fixed-20240613-135945.yaml | 5 ----- .../AccompanyingPeriodWorkEndDateBetweenDateFilter.php | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) delete mode 100644 .changes/unreleased/Fixed-20240613-135945.yaml diff --git a/.changes/unreleased/Fixed-20240613-135945.yaml b/.changes/unreleased/Fixed-20240613-135945.yaml deleted file mode 100644 index 38028a735..000000000 --- a/.changes/unreleased/Fixed-20240613-135945.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Fixed -body: Take into account the acp closing date in the acp works date filter -time: 2024-06-13T13:59:45.561891547+02:00 -custom: - Issue: "271" diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkEndDateBetweenDateFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkEndDateBetweenDateFilter.php index 38ca39320..92258ccf9 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkEndDateBetweenDateFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkEndDateBetweenDateFilter.php @@ -86,11 +86,11 @@ final readonly class AccompanyingPeriodWorkEndDateBetweenDateFilter implements F }; $end = match ($data['keep_null']) { true => $qb->expr()->orX( - $qb->expr()->gt('COALESCE(acp.closingDate, acpw.endDate)', ':'.$as), + $qb->expr()->gt('acpw.endDate', ':'.$as), $qb->expr()->isNull('acpw.endDate') ), false => $qb->expr()->andX( - $qb->expr()->gt('COALESCE(acp.closingDate, acpw.endDate)', ':'.$as), + $qb->expr()->gt('acpw.endDate', ':'.$as), $qb->expr()->isNotNull('acpw.endDate') ), default => throw new \LogicException('This value is not supported'), From 702a5a27d281d8ba5bf44265c185b639cc051b1a Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 3 Jul 2024 12:22:53 +0200 Subject: [PATCH 13/13] Update version of bundles to 2.22.2 --- .changes/v2.22.2.md | 3 +++ CHANGELOG.md | 10 ++++++++++ 2 files changed, 13 insertions(+) create mode 100644 .changes/v2.22.2.md diff --git a/.changes/v2.22.2.md b/.changes/v2.22.2.md new file mode 100644 index 000000000..51b3c75da --- /dev/null +++ b/.changes/v2.22.2.md @@ -0,0 +1,3 @@ +## v2.22.2 - 2024-07-03 +### Fixed +* Remove scope required for event participation stats diff --git a/CHANGELOG.md b/CHANGELOG.md index 47b3760b0..1d0fd76e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). +## v2.22.2 - 2024-07-03 +### Fixed +* Remove scope required for event participation stats + +## v2.22.1 - 2024-07-01 +### Fixed +* Remove debug word +### DX +* Add a command for reading official address DB from Luxembourg and update chill addresses + ## v2.22.0 - 2024-06-25 ### Feature * ([#216](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/216)) [event bundle] exports added for the event module