Merge branch '216-exports-event-bundle' into 'master'

Add eventBundle exports

Closes #216

See merge request Chill-Projet/chill-bundles!688
This commit is contained in:
LenaertsJ 2024-06-25 11:24:57 +00:00
commit b1ba5cc608
33 changed files with 1512 additions and 121 deletions

View File

@ -76,7 +76,7 @@
"phpunit/phpunit": ">= 7.5",
"psalm/plugin-phpunit": "^0.18.4",
"psalm/plugin-symfony": "^4.0.2",
"rector/rector": "^1.1.0",
"rector/rector": "1.1.1",
"symfony/debug-bundle": "^5.1",
"symfony/dotenv": "^4.4",
"symfony/maker-bundle": "^1.20",

View File

@ -32,9 +32,13 @@ return static function (RectorConfig $rectorConfig): void {
//define sets of rules
$rectorConfig->sets([
LevelSetList::UP_TO_PHP_82,
\Rector\Symfony\Set\SymfonyLevelSetList::UP_TO_SYMFONY_44,
\Rector\Symfony\Set\SymfonySetList::SYMFONY_40,
\Rector\Symfony\Set\SymfonySetList::SYMFONY_41,
\Rector\Symfony\Set\SymfonySetList::SYMFONY_42,
\Rector\Symfony\Set\SymfonySetList::SYMFONY_43,
\Rector\Symfony\Set\SymfonySetList::SYMFONY_44,
\Rector\Doctrine\Set\DoctrineSetList::DOCTRINE_CODE_QUALITY,
\Rector\PHPUnit\Set\PHPUnitLevelSetList::UP_TO_PHPUNIT_90,
\Rector\PHPUnit\Set\PHPUnitSetList::PHPUNIT_90,
]);
// some routes are added twice if it remains activated

View File

@ -15,7 +15,7 @@ use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\Participation;
use Chill\EventBundle\Form\EventType;
use Chill\EventBundle\Form\Type\PickEventType;
use Chill\EventBundle\Security\Authorization\EventVoter;
use Chill\EventBundle\Security\EventVoter;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Pagination\PaginatorFactory;
@ -433,7 +433,6 @@ final class EventController extends AbstractController
$builder->add('event_id', HiddenType::class, [
'data' => $event->getId(),
]);
dump($event->getId());
return $builder->getForm();
}

View File

@ -15,7 +15,7 @@ use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\Participation;
use Chill\EventBundle\Form\ParticipationType;
use Chill\EventBundle\Repository\EventRepository;
use Chill\EventBundle\Security\Authorization\ParticipationVoter;
use Chill\EventBundle\Security\ParticipationVoter;
use Chill\PersonBundle\Repository\PersonRepository;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Doctrine\Common\Collections\Collection;

View File

@ -11,8 +11,8 @@ declare(strict_types=1);
namespace Chill\EventBundle\DependencyInjection;
use Chill\EventBundle\Security\Authorization\EventVoter;
use Chill\EventBundle\Security\Authorization\ParticipationVoter;
use Chill\EventBundle\Security\EventVoter;
use Chill\EventBundle\Security\ParticipationVoter;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
@ -33,12 +33,13 @@ class ChillEventExtension extends Extension implements PrependExtensionInterface
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
$loader->load('services.yaml');
$loader->load('services/authorization.yaml');
$loader->load('services/security.yaml');
$loader->load('services/fixtures.yaml');
$loader->load('services/forms.yaml');
$loader->load('services/repositories.yaml');
$loader->load('services/search.yaml');
$loader->load('services/timeline.yaml');
$loader->load('services/export.yaml');
}
/** (non-PHPdoc).

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,83 @@
<?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,83 @@
<?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,127 @@
<?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,128 @@
<?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,97 @@
<?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,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\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,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\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

@ -11,7 +11,7 @@ declare(strict_types=1);
namespace Chill\EventBundle\Menu;
use Chill\EventBundle\Security\Authorization\EventVoter;
use Chill\EventBundle\Security\EventVoter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

View File

@ -11,7 +11,7 @@ declare(strict_types=1);
namespace Chill\EventBundle\Menu;
use Chill\EventBundle\Security\Authorization\EventVoter;
use Chill\EventBundle\Security\EventVoter;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Security;

View File

@ -13,7 +13,7 @@ namespace Chill\EventBundle\Repository;
use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\Participation;
use Chill\EventBundle\Security\Authorization\EventVoter;
use Chill\EventBundle\Security\EventVoter;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
use Chill\PersonBundle\Entity\Person;

View File

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

View File

@ -9,18 +9,19 @@ declare(strict_types=1);
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Security\Authorization;
namespace Chill\EventBundle\Security;
use Chill\EventBundle\Entity\Event;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
/**
* Description of EventVoter.
@ -42,61 +43,46 @@ class EventVoter extends AbstractChillVoter implements ProvideRoleHierarchyInter
final public const UPDATE = 'CHILL_EVENT_UPDATE';
/**
* @var AccessDecisionManagerInterface
*/
protected $accessDecisionManager;
final public const STATS = 'CHILL_EVENT_STATS';
/**
* @var AuthorizationHelper
*/
protected $authorizationHelper;
/**
* @var LoggerInterface
*/
protected $logger;
private readonly VoterHelperInterface $voterHelper;
public function __construct(
AccessDecisionManagerInterface $accessDecisionManager,
AuthorizationHelper $authorizationHelper,
LoggerInterface $logger
private readonly AuthorizationHelper $authorizationHelper,
private readonly LoggerInterface $logger,
VoterHelperFactoryInterface $voterHelperFactory
) {
$this->accessDecisionManager = $accessDecisionManager;
$this->authorizationHelper = $authorizationHelper;
$this->logger = $logger;
$this->voterHelper = $voterHelperFactory
->generate(self::class)
->addCheckFor(null, [self::SEE])
->addCheckFor(Event::class, [...self::ROLES])
->addCheckFor(Person::class, [self::SEE, self::CREATE])
->addCheckFor(Center::class, [self::STATS])
->build();
}
public function getRoles(): array
{
return self::ROLES;
return [...self::ROLES, self::STATS];
}
public function getRolesWithHierarchy(): array
{
return [
'Event' => self::ROLES,
'Event' => $this->getRoles(),
];
}
public function getRolesWithoutScope(): array
{
return [];
return [self::ROLES, self::STATS];
}
public function supports($attribute, $subject)
{
return ($subject instanceof Event && \in_array($attribute, self::ROLES, true))
|| ($subject instanceof Person && \in_array($attribute, [self::CREATE, self::SEE], true))
|| (null === $subject && self::SEE === $attribute);
return $this->voterHelper->supports($attribute, $subject);
}
/**
* @param string $attribute
* @param Event $subject
*
* @return bool
*/
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
$this->logger->debug(sprintf('Voting from %s class', self::class));
@ -118,15 +104,5 @@ class EventVoter extends AbstractChillVoter implements ProvideRoleHierarchyInter
->getReachableCenters($token->getUser(), $attribute);
return \count($centers) > 0;
if (!$this->accessDecisionManager->decide($token, [PersonVoter::SEE], $person)) {
return false;
}
return $this->authorizationHelper->userHasAccess(
$token->getUser(),
$subject,
$attribute
);
}
}

View File

@ -9,18 +9,19 @@ declare(strict_types=1);
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\EventBundle\Security\Authorization;
namespace Chill\EventBundle\Security;
use Chill\EventBundle\Entity\Participation;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
class ParticipationVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface
{
@ -39,40 +40,33 @@ class ParticipationVoter extends AbstractChillVoter implements ProvideRoleHierar
final public const UPDATE = 'CHILL_EVENT_PARTICIPATION_UPDATE';
/**
* @var AccessDecisionManagerInterface
*/
protected $accessDecisionManager;
final public const STATS = 'CHILL_EVENT_PARTICIPATION_STATS';
/**
* @var AuthorizationHelper
*/
protected $authorizationHelper;
/**
* @var LoggerInterface
*/
protected $logger;
private readonly VoterHelperInterface $voterHelper;
public function __construct(
AccessDecisionManagerInterface $accessDecisionManager,
AuthorizationHelper $authorizationHelper,
LoggerInterface $logger
private readonly AuthorizationHelper $authorizationHelper,
private readonly LoggerInterface $logger,
VoterHelperFactoryInterface $voterHelperFactory
) {
$this->accessDecisionManager = $accessDecisionManager;
$this->authorizationHelper = $authorizationHelper;
$this->logger = $logger;
$this->voterHelper = $voterHelperFactory
->generate(self::class)
->addCheckFor(null, [self::SEE])
->addCheckFor(Participation::class, [...self::ROLES])
->addCheckFor(Person::class, [self::SEE, self::CREATE])
->addCheckFor(Center::class, [self::STATS])
->build();
}
public function getRoles(): array
{
return self::ROLES;
return [...self::ROLES, self::STATS];
}
public function getRolesWithHierarchy(): array
{
return [
'Event' => self::ROLES,
'Participation' => $this->getRoles(),
];
}
@ -83,14 +77,11 @@ class ParticipationVoter extends AbstractChillVoter implements ProvideRoleHierar
public function supports($attribute, $subject)
{
return ($subject instanceof Participation && \in_array($attribute, self::ROLES, true))
|| ($subject instanceof Person && \in_array($attribute, [self::CREATE, self::SEE], true))
|| (null === $subject && self::SEE === $attribute);
return $this->voterHelper->supports($attribute, $subject);
}
/**
* @param string $attribute
* @param Participation $subject
*
* @return bool
*/
@ -115,15 +106,5 @@ class ParticipationVoter extends AbstractChillVoter implements ProvideRoleHierar
->getReachableCenters($token->getUser(), $attribute);
return \count($centers) > 0;
if (!$this->accessDecisionManager->decide($token, [PersonVoter::SEE], $person)) {
return false;
}
return $this->authorizationHelper->userHasAccess(
$token->getUser(),
$subject,
$attribute
);
}
}

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::$container->get(CountEventParticipations::class);
}
public function testExecuteQuery(): void
{
$qb = $this->countEventParticipations->initiateQuery([], [], [])
->setMaxResults(1);
$results = $qb->getQuery()->getResult(AbstractQuery::HYDRATE_ARRAY);
self::assertIsArray($results, 'smoke test: test that the result is an array');
}
}

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::$container->get(CountEvents::class);
}
public function testExecuteQuery(): void
{
$qb = $this->countEvents->initiateQuery([], [], [])
->setMaxResults(1);
$results = $qb->getQuery()->getResult(AbstractQuery::HYDRATE_ARRAY);
self::assertIsArray($results, 'smoke test: test that the result is an array');
}
}

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::$container->get(EventDateAggregator::class);
}
public function getAggregator()
{
return $this->aggregator;
}
public function getFormData(): array|\Generator
{
yield ['frequency' => 'YYYY'];
yield ['frequency' => 'YYYY-MM'];
yield ['frequency' => 'YYYY-IV'];
}
public function getQueryBuilders(): array
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
return [
$em->createQueryBuilder()
->select('event.id')
->from(Event::class, 'event'),
];
}
}

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::$container->get(EventTypeAggregator::class);
}
public function getAggregator()
{
return $this->aggregator;
}
public function getFormData(): array
{
return [
[],
];
}
public function getQueryBuilders(): array
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
return [
$em->createQueryBuilder()
->select('event.id')
->from(Event::class, 'event'),
];
}
}

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::$container->get(RoleAggregator::class);
}
public function getAggregator()
{
return $this->aggregator;
}
public function getFormData(): array
{
return [
[],
];
}
public function getQueryBuilders(): array
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
return [
$em->createQueryBuilder()
->select('event.id')
->from(Event::class, 'event'),
$em->createQueryBuilder()
->select('event_part')
->from(Participation::class, 'event_part'),
];
}
}

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::$container->get(RollingDateConverterInterface::class);
}
public function getFilter()
{
return new EventDateFilter($this->rollingDateConverter);
}
public function getFormData()
{
return [
[
'date_from' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'date_to' => new RollingDate(RollingDate::T_TODAY),
],
];
}
public function getQueryBuilders(): array
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
return [
$em->createQueryBuilder()
->select('event.id')
->from(Event::class, 'event'),
];
}
}

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::$container->get(EventTypeFilter::class);
}
public function getFilter(): EventTypeFilter|\Chill\MainBundle\Export\FilterInterface
{
return $this->filter;
}
public function getFormData()
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
$array = $em->createQueryBuilder()
->from(EventType::class, 'et')
->select('et')
->getQuery()
->getResult();
$data = [];
foreach ($array as $a) {
$data[] = [
'types' => new ArrayCollection([$a]),
];
}
return $data;
}
public function getQueryBuilders()
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
return [
$em->createQueryBuilder()
->select('event.id')
->from(Event::class, 'event'),
];
}
}

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::$container->get(RoleFilter::class);
}
public function getFilter()
{
return $this->filter;
}
public function getFormData(): array
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
$array = $em->createQueryBuilder()
->from(Role::class, 'r')
->select('r')
->getQuery()
->setMaxResults(1)
->getResult();
$data = [];
foreach ($array as $a) {
$data[] = [
'roles' => new ArrayCollection([$a]),
];
}
return $data;
}
public function getQueryBuilders()
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
return [
$em->createQueryBuilder()
->select('event.id')
->from(Event::class, 'event'),
$em->createQueryBuilder()
->select('event_part')
->from(Participation::class, 'event_part'),
];
}
}

View File

@ -12,7 +12,7 @@ declare(strict_types=1);
namespace Chill\EventBundle\Tests\Repository;
use Chill\EventBundle\Repository\EventACLAwareRepository;
use Chill\EventBundle\Security\Authorization\EventVoter;
use Chill\EventBundle\Security\EventVoter;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;

View File

@ -1,20 +0,0 @@
services:
chill_event.event_voter:
class: Chill\EventBundle\Security\Authorization\EventVoter
arguments:
- "@security.access.decision_manager"
- "@chill.main.security.authorization.helper"
- "@logger"
tags:
- { name: chill.role }
- { name: security.voter }
chill_event.event_participation:
class: Chill\EventBundle\Security\Authorization\ParticipationVoter
arguments:
- "@security.access.decision_manager"
- "@chill.main.security.authorization.helper"
- "@logger"
tags:
- { name: chill.role }
- { name: security.voter }

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

@ -0,0 +1,14 @@
services:
Chill\EventBundle\Security\EventVoter:
autowire: true
autoconfigure: true
tags:
- { name: security.voter }
- { name: chill.role }
Chill\EventBundle\Security\ParticipationVoter:
autowire: true
autoconfigure: true
tags:
- { name: security.voter }
- { name: chill.role }

View File

@ -81,9 +81,31 @@ Pick an event: Choisir un événement
Pick a type of event: Choisir un type d'événement
Pick a moderator: Choisir un animateur
# exports
Select a format: Choisir un format
Export: Exporter
Count events: Nombre d'événements
Count events by various parameters.: Compte le nombre d'événements selon divers critères
Exports of events: Exports d'événements
Filtered by event date: Filtrer par date d'événement
'Filtered by date of event: only between %date_from% and %date_to%': "Filtré par date d'événement: uniquement entre le %date_from% et le %date_to%"
Events after this date: Événements après cette date
Events before this date: Événements avant cette date
Filtered by event type: Filtrer par type d'événement
'Filtered by event type: only %list%': "Filtré par type: uniquement %list%"
Group event by date: Grouper par date d'événement
Group by event type: Grouper par type d'événement
Count event participants: Nombre de participations
Count participants to an event by various parameters.: Compte le nombre de participations selon divers critères
Exports of event participants: Exports de participations
'Filtered by participant roles: only %list%': "Filtré par rôles de participation: uniquement %list%"
Filter by participant roles: Filtrer par rôles de participation
Part roles: Rôles de participation
Group by participant role: Grouper par rôle de participation
Events configuration: Configuration des événements
Events configuration menu: Menu des événements