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