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/.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 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 dc8f2305a..1d0fd76e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,23 @@ 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 + +### 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 diff --git a/docs/source/installation/index.rst b/docs/source/installation/index.rst index 072a32ffb..a9a4ea0c6 100644 --- a/docs/source/installation/index.rst +++ b/docs/source/installation/index.rst @@ -56,7 +56,7 @@ We strongly encourage you to initialize a git repository at this step, to track cat <<< "$(jq '.extra.symfony += {"endpoint": ["flex://defaults", "https://gitlab.com/api/v4/projects/57371968/repository/files/index.json/raw?ref=main"]}' composer.json)" > composer.json # install chill and some dependencies # TODO fix the suffix "alpha1" and replace by ^3.0.0 when version 3.0.0 will be released - symfony composer require chill-project/chill-bundles v3.0.0-alpha1 champs-libres/wopi-lib dev-master@dev champs-libres/wopi-bundle dev-master@dev + symfony composer require chill-project/chill-bundles v3.0.0-RC3 champs-libres/wopi-lib dev-master@dev champs-libres/wopi-bundle dev-master@dev We encourage you to accept the inclusion of the "Docker configuration from recipes": this is the documented way to run the database. You must also accept to configure recipes from the contrib repository, unless you want to configure the bundles manually). @@ -110,15 +110,14 @@ you can either: .. code-block:: env ADMIN_PASSWORD=\$2y\$13\$iyvJLuT4YEa6iWXyQV4/N.hNHpNG8kXlYDkkt5MkYy4FXcSwYAwmm + # note: if you copy-paste the line above, the password will be "admin". - add the generated password to the secrets manager (**note**: you must add the generated hashed password to the secrets env, not the password in clear text). - set up the jwt authentication bundle -Some environment variables are available for the JWT authentication bundle in the :code:`.env` file. You must also run the command -:code:`symfony console lexik:jwt:generate-keypair` to generate some keys that will be stored in the paths set up in the :code:`JWT_SECRET_KEY` -and the :code:`JWT_PUBLIC_KEY` env variables. This is only required for using the stored documents in Chill. +Some environment variables are available for the JWT authentication bundle in the :code:`.env` file. Prepare migrations and other tools ********************************** @@ -136,6 +135,8 @@ To continue the installation process, you will have to run migrations: symfony console messenger:setup-transports # prepare some views symfony console chill:db:sync-views + # generate jwt token, required for some api features (webdav access, ...) + symfony console lexik:jwt:generate-keypair .. warning:: 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 %} diff --git a/src/Bundle/ChillEventBundle/DependencyInjection/ChillEventExtension.php b/src/Bundle/ChillEventBundle/DependencyInjection/ChillEventExtension.php index f36e82075..0b30ca6c5 100644 --- a/src/Bundle/ChillEventBundle/DependencyInjection/ChillEventExtension.php +++ b/src/Bundle/ChillEventBundle/DependencyInjection/ChillEventExtension.php @@ -39,6 +39,7 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface $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..db757cac9 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Export/Aggregator/EventTypeAggregator.php @@ -0,0 +1,81 @@ +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..c02483db2 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Export/Aggregator/RoleAggregator.php @@ -0,0 +1,81 @@ +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..6ecfa3f43 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Export/Export/CountEvents.php @@ -0,0 +1,126 @@ +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..10f1dbd81 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Export/Filter/EventDateFilter.php @@ -0,0 +1,95 @@ +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..9e8855adf --- /dev/null +++ b/src/Bundle/ChillEventBundle/Export/Filter/EventTypeFilter.php @@ -0,0 +1,94 @@ +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..791f07f7a --- /dev/null +++ b/src/Bundle/ChillEventBundle/Export/Filter/RoleFilter.php @@ -0,0 +1,94 @@ +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/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/Tests/Export/CountEventParticipationsTest.php b/src/Bundle/ChillEventBundle/Tests/Export/CountEventParticipationsTest.php new file mode 100644 index 000000000..2c7886ed7 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Tests/Export/CountEventParticipationsTest.php @@ -0,0 +1,43 @@ +countEventParticipations = self::getContainer()->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..15884edec --- /dev/null +++ b/src/Bundle/ChillEventBundle/Tests/Export/CountEventTest.php @@ -0,0 +1,43 @@ +countEvents = self::getContainer()->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..4adf4fa2c --- /dev/null +++ b/src/Bundle/ChillEventBundle/Tests/Export/aggregators/EventDateAggregatorTest.php @@ -0,0 +1,59 @@ +aggregator = self::getContainer()->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::getContainer()->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..33e3182c4 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Tests/Export/aggregators/EventTypeAggregatorTest.php @@ -0,0 +1,59 @@ +aggregator = self::getContainer()->get(EventTypeAggregator::class); + } + + public function getAggregator() + { + return $this->aggregator; + } + + public function getFormData(): array + { + return [ + [], + ]; + } + + public function getQueryBuilders(): array + { + self::bootKernel(); + + $em = self::getContainer()->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..c9cdb92be --- /dev/null +++ b/src/Bundle/ChillEventBundle/Tests/Export/aggregators/RoleAggregatorTest.php @@ -0,0 +1,63 @@ +aggregator = self::getContainer()->get(RoleAggregator::class); + } + + public function getAggregator() + { + return $this->aggregator; + } + + public function getFormData(): array + { + return [ + [], + ]; + } + + public function getQueryBuilders(): array + { + self::bootKernel(); + + $em = self::getContainer()->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..369ceee59 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Tests/Export/filters/EventDateFilterTest.php @@ -0,0 +1,65 @@ +rollingDateConverter = self::getContainer()->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::getContainer()->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..4e41b624e --- /dev/null +++ b/src/Bundle/ChillEventBundle/Tests/Export/filters/EventTypeFilterTest.php @@ -0,0 +1,76 @@ +filter = self::getContainer()->get(EventTypeFilter::class); + } + + public function getFilter(): EventTypeFilter|\Chill\MainBundle\Export\FilterInterface + { + return $this->filter; + } + + public function getFormData() + { + self::bootKernel(); + + $em = self::getContainer()->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::getContainer()->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..714098956 --- /dev/null +++ b/src/Bundle/ChillEventBundle/Tests/Export/filters/RoleFilterTest.php @@ -0,0 +1,81 @@ +filter = self::getContainer()->get(RoleFilter::class); + } + + public function getFilter() + { + return $this->filter; + } + + public function getFormData(): array + { + self::bootKernel(); + $em = self::getContainer()->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::getContainer()->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/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 index a139b72a9..13198d55f 100644 --- a/src/Bundle/ChillEventBundle/config/services/security.yaml +++ b/src/Bundle/ChillEventBundle/config/services/security.yaml @@ -12,4 +12,3 @@ services: 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 f12b8f866..6046881ff 100644 --- a/src/Bundle/ChillEventBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillEventBundle/translations/messages.fr.yml @@ -80,9 +80,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 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..50ee16401 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceLU.php @@ -0,0 +1,95 @@ +client->request('GET', $downloadUrl); + + if (200 !== $response->getStatusCode()) { + throw new \Exception('Could not download CSV: '.$response->getStatusCode()); + } + + $file = tmpfile(); + + foreach ($this->client->stream($response) as $chunk) { + fwrite($file, $chunk->getContent()); + } + + fseek($file, 0); + + $csv = Reader::createFromStream($file); + $csv->setDelimiter(';'); + $csv->setHeaderOffset(0); + + $this->process_postal_code($csv); + + $this->process_address($csv); + + $this->addressToReferenceMatcher->checkAddressesMatchingReferences(); + + fclose($file); + } + + 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.lux', + (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 === \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.lux', + (float) $record['lat_wgs84'], + (float) $record['lon_wgs84'], + 4326 + ); + $arr_postal_codes[$record['code_postal']] = 1; + } + } + } +} 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