merge latest changes from upgrade-sf5 branch

This commit is contained in:
Julie Lenaerts 2024-07-17 13:17:59 +02:00
commit b8002d56ec
31 changed files with 1607 additions and 11 deletions

6
.changes/v2.22.0.md Normal file
View File

@ -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.

5
.changes/v2.22.1.md Normal file
View File

@ -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

3
.changes/v2.22.2.md Normal file
View File

@ -0,0 +1,3 @@
## v2.22.2 - 2024-07-03
### Fixed
* Remove scope required for event participation stats

View File

@ -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). 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 ## v2.21.0 - 2024-06-18
### Feature ### Feature
* Add flash menu buttons in search results, to open directly a new calendar, or a new activity in an accompanying period * Add flash menu buttons in search results, to open directly a new calendar, or a new activity in an accompanying period

View File

@ -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 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 # install chill and some dependencies
# TODO fix the suffix "alpha1" and replace by ^3.0.0 when version 3.0.0 will be released # 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. 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). 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 .. code-block:: env
ADMIN_PASSWORD=\$2y\$13\$iyvJLuT4YEa6iWXyQV4/N.hNHpNG8kXlYDkkt5MkYy4FXcSwYAwmm 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, - 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). not the password in clear text).
- set up the jwt authentication bundle - 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 Some environment variables are available for the JWT authentication bundle in the :code:`.env` file.
: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.
Prepare migrations and other tools 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 symfony console messenger:setup-transports
# prepare some views # prepare some views
symfony console chill:db:sync-views symfony console chill:db:sync-views
# generate jwt token, required for some api features (webdav access, ...)
symfony console lexik:jwt:generate-keypair
.. warning:: .. warning::

View File

@ -87,7 +87,6 @@
<li> <li>
{% if bloc.type == 'user' %} {% if bloc.type == 'user' %}
<span class="badge-user"> <span class="badge-user">
hello
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }} {{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }}
</span> </span>
{% else %} {% else %}

View File

@ -39,6 +39,7 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
$loader->load('services/repositories.yaml'); $loader->load('services/repositories.yaml');
$loader->load('services/search.yaml'); $loader->load('services/search.yaml');
$loader->load('services/timeline.yaml'); $loader->load('services/timeline.yaml');
$loader->load('services/export.yaml');
} }
/** (non-PHPdoc). /** (non-PHPdoc).

View File

@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export\Aggregator;
use Chill\EventBundle\Export\Declarations;
use Chill\MainBundle\Export\AggregatorInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
class EventDateAggregator implements AggregatorInterface
{
private const CHOICES = [
'by month' => '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';
}
}

View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export\Aggregator;
use Chill\EventBundle\Export\Declarations;
use Chill\EventBundle\Repository\EventTypeRepository;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class EventTypeAggregator implements AggregatorInterface
{
final public const KEY = 'event_type_aggregator';
public function __construct(protected EventTypeRepository $eventTypeRepository, protected TranslatableStringHelperInterface $translatableStringHelper) {}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
if (!\in_array('eventtype', $qb->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';
}
}

View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export\Aggregator;
use Chill\EventBundle\Export\Declarations;
use Chill\EventBundle\Repository\RoleRepository;
use Chill\MainBundle\Export\AggregatorInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
class RoleAggregator implements AggregatorInterface
{
final public const KEY = 'part_role_aggregator';
public function __construct(protected RoleRepository $roleRepository, protected TranslatableStringHelperInterface $translatableStringHelper) {}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
if (!\in_array('event_part', $qb->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';
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export;
/**
* This class declare constants used for the export framework.
*/
abstract class Declarations
{
final public const EVENT = 'event';
final public const EVENT_PARTICIPANTS = 'event_participants';
}

View File

@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export\Export;
use Chill\EventBundle\Export\Declarations;
use Chill\EventBundle\Repository\ParticipationRepository;
use Chill\EventBundle\Security\ParticipationVoter;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\GroupedExportInterface;
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
use Doctrine\ORM\Query;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Chill\PersonBundle\Export\Declarations as PersonDeclarations;
readonly class CountEventParticipations implements ExportInterface, GroupedExportInterface
{
private bool $filterStatsByCenters;
public function __construct(
private ParticipationRepository $participationRepository,
ParameterBagInterface $parameterBag,
) {
$this->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,
];
}
}

View File

@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export\Export;
use Chill\EventBundle\Repository\EventRepository;
use Chill\EventBundle\Security\EventVoter;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\FormatterInterface;
use Chill\MainBundle\Export\GroupedExportInterface;
use Chill\PersonBundle\Entity\Person\PersonCenterHistory;
use Doctrine\ORM\Query;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Chill\EventBundle\Export\Declarations;
use Chill\PersonBundle\Export\Declarations as PersonDeclarations;
readonly class CountEvents implements ExportInterface, GroupedExportInterface
{
private bool $filterStatsByCenters;
public function __construct(
private EventRepository $eventRepository,
ParameterBagInterface $parameterBag,
) {
$this->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,
];
}
}

View File

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export\Filter;
use Chill\EventBundle\Export\Declarations;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Form\Type\PickRollingDateType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class EventDateFilter implements FilterInterface
{
public function __construct(protected TranslatorInterface $translator, private readonly RollingDateConverterInterface $rollingDateConverter) {}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$where = $qb->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';
}
}

View File

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export\Filter;
use Chill\EventBundle\Entity\EventType;
use Chill\EventBundle\Export\Declarations;
use Chill\EventBundle\Repository\EventTypeRepository;
use Chill\MainBundle\Export\ExportElementValidatedInterface;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
class EventTypeFilter implements ExportElementValidatedInterface, FilterInterface
{
public function __construct(
protected TranslatableStringHelperInterface $translatableStringHelper,
protected EventTypeRepository $eventTypeRepository
) {}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$clause = $qb->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();
}
}
}

View File

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Export\Filter;
use Chill\EventBundle\Entity\Role;
use Chill\EventBundle\Export\Declarations;
use Chill\EventBundle\Repository\RoleRepository;
use Chill\MainBundle\Export\ExportElementValidatedInterface;
use Chill\MainBundle\Export\FilterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
class RoleFilter implements ExportElementValidatedInterface, FilterInterface
{
public function __construct(
protected TranslatableStringHelperInterface $translatableStringHelper,
protected RoleRepository $roleRepository
) {}
public function addRole(): ?string
{
return null;
}
public function alterQuery(QueryBuilder $qb, $data)
{
$clause = $qb->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();
}
}
}

View File

@ -12,13 +12,57 @@ declare(strict_types=1);
namespace Chill\EventBundle\Repository; namespace Chill\EventBundle\Repository;
use Chill\EventBundle\Entity\Role; use Chill\EventBundle\Entity\Role;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Chill\MainBundle\Templating\TranslatableStringHelper;
use Doctrine\Persistence\ManagerRegistry; 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;
} }
} }

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Tests\Export;
use Chill\EventBundle\Export\Export\CountEventParticipations;
use Doctrine\ORM\AbstractQuery;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @internal
*
* @coversNothing
*/
class CountEventParticipationsTest extends KernelTestCase
{
private CountEventParticipations $countEventParticipations;
protected function setUp(): void
{
parent::setUp();
self::bootKernel();
$this->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');
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Tests\Export;
use Chill\EventBundle\Export\Export\CountEvents;
use Doctrine\ORM\AbstractQuery;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @internal
*
* @coversNothing
*/
class CountEventTest extends KernelTestCase
{
private CountEvents $countEvents;
protected function setUp(): void
{
parent::setUp();
self::bootKernel();
$this->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');
}
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Export\aggregators;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Export\Aggregator\EventDateAggregator;
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
use Doctrine\ORM\EntityManagerInterface;
/**
* @internal
*
* @coversNothing
*/
class EventDateAggregatorTest extends AbstractAggregatorTest
{
private $aggregator;
protected function setUp(): void
{
self::bootKernel();
$this->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'),
];
}
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Export\aggregators;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Export\Aggregator\EventTypeAggregator;
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
use Doctrine\ORM\EntityManagerInterface;
/**
* @internal
*
* @coversNothing
*/
class EventTypeAggregatorTest extends AbstractAggregatorTest
{
private $aggregator;
protected function setUp(): void
{
self::bootKernel();
$this->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'),
];
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Export\aggregators;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\Participation;
use Chill\EventBundle\Export\Aggregator\RoleAggregator;
use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
use Doctrine\ORM\EntityManagerInterface;
/**
* @internal
*
* @coversNothing
*/
class RoleAggregatorTest extends AbstractAggregatorTest
{
private $aggregator;
protected function setUp(): void
{
self::bootKernel();
$this->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'),
];
}
}

View File

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Export\filters;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Export\Filter\EventDateFilter;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\MainBundle\Test\Export\AbstractFilterTest;
use Doctrine\ORM\EntityManagerInterface;
/**
* @internal
*
* @coversNothing
*/
class EventDateFilterTest extends AbstractFilterTest
{
private RollingDateConverterInterface $rollingDateConverter;
protected function setUp(): void
{
parent::setUp();
self::bootKernel();
$this->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'),
];
}
}

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Export\filters;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\EventType;
use Chill\EventBundle\Export\Filter\EventTypeFilter;
use Chill\MainBundle\Test\Export\AbstractFilterTest;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
/**
* @internal
*
* @coversNothing
*/
class EventTypeFilterTest extends AbstractFilterTest
{
private EventTypeFilter $filter;
protected function setUp(): void
{
self::bootKernel();
$this->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'),
];
}
}

View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Export\filters;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\Participation;
use Chill\EventBundle\Entity\Role;
use Chill\EventBundle\Export\Filter\RoleFilter;
use Chill\MainBundle\Test\Export\AbstractFilterTest;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
/**
* @internal
*
* @coversNothing
*/
class RoleFilterTest extends AbstractFilterTest
{
private RoleFilter $filter;
protected function setUp(): void
{
self::bootKernel();
$this->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'),
];
}
}

View File

@ -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 }

View File

@ -12,4 +12,3 @@ services:
tags: tags:
- { name: security.voter } - { name: security.voter }
- { name: chill.role } - { name: chill.role }

View File

@ -80,9 +80,31 @@ Pick an event: Choisir un événement
Pick a type of event: Choisir un type d'événement Pick a type of event: Choisir un type d'événement
Pick a moderator: Choisir un animateur Pick a moderator: Choisir un animateur
# exports
Select a format: Choisir un format Select a format: Choisir un format
Export: Exporter 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: Configuration des événements
Events configuration menu: Menu des événements Events configuration menu: Menu des événements

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Command;
use Chill\MainBundle\Service\Import\AddressReferenceLU;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class LoadAddressesLUFromBDAddressCommand extends Command
{
protected static $defaultDescription = 'Import LUX addresses from BD addresses (see https://data.public.lu/fr/datasets/adresses-georeferencees-bd-adresses/)';
public function __construct(
private readonly AddressReferenceLU $addressImporter,
) {
parent::__construct();
}
protected function configure()
{
$this->setName('chill:main:address-ref-lux');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->addressImporter->import();
return Command::SUCCESS;
}
}

View File

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Service\Import;
use League\Csv\Reader;
use League\Csv\Statement;
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 import(): void
{
$downloadUrl = self::RELEASE;
$response = $this->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;
}
}
}
}

View File

@ -59,6 +59,12 @@ services:
tags: tags:
- { name: console.command } - { name: console.command }
Chill\MainBundle\Command\LoadAddressesLUFromBDAddressCommand:
autoconfigure: true
autowire: true
tags:
- { name: console.command }
Chill\MainBundle\Command\ExecuteCronJobCommand: Chill\MainBundle\Command\ExecuteCronJobCommand:
autoconfigure: true autoconfigure: true
autowire: true autowire: true