From 90b615c5b214bbbbf280bbfab193b6a1c1357cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 14 Jun 2024 13:58:48 +0200 Subject: [PATCH 01/10] Add data transformation interface for filters Introduced a new DataTransformerFilterInterface that allows transforming filter's form data before it is processed. Updated the FilterType file to add a view transformer if the filter implements this new interface. This new transformation process caters to transforming existing data in saved exports and replacing it with default values. --- .../Export/DataTransformerFilterInterface.php | 25 +++++++++++++++++++ .../Export/FilterInterface.php | 3 +++ .../Form/Type/Export/FilterType.php | 11 ++++++++ 3 files changed, 39 insertions(+) create mode 100644 src/Bundle/ChillMainBundle/Export/DataTransformerFilterInterface.php diff --git a/src/Bundle/ChillMainBundle/Export/DataTransformerFilterInterface.php b/src/Bundle/ChillMainBundle/Export/DataTransformerFilterInterface.php new file mode 100644 index 000000000..055ee2edc --- /dev/null +++ b/src/Bundle/ChillMainBundle/Export/DataTransformerFilterInterface.php @@ -0,0 +1,25 @@ +buildForm($filterFormBuilder); + if ($filter instanceof DataTransformerFilterInterface) { + $filterFormBuilder->addViewTransformer(new CallbackTransformer( + fn (?array $data) => $data, + function (?array $data) use ($filter) { + return $filter->transformData($data); + }, + )); + } + $builder->add($filterFormBuilder); } From cbd9489810fcf0e80b1a4f36d9a57d4340ef9080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 5 Jun 2024 16:04:18 +0200 Subject: [PATCH 02/10] Add startDate and endDate on UserJobFilter --- .../UserJobFilter.php | 108 +++++++++++------- .../UserJobFilterTest.php | 6 +- .../translations/messages+intl-icu.fr.yaml | 2 + .../translations/messages.fr.yml | 3 +- 4 files changed, 74 insertions(+), 45 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserJobFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserJobFilter.php index d6bc25198..120d0a70e 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserJobFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserJobFilter.php @@ -13,23 +13,28 @@ namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters; use Chill\MainBundle\Entity\User\UserJobHistory; use Chill\MainBundle\Entity\UserJob; +use Chill\MainBundle\Export\DataTransformerFilterInterface; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Form\Type\PickRollingDateType; use Chill\MainBundle\Repository\UserJobRepositoryInterface; +use Chill\MainBundle\Service\RollingDate\RollingDate; +use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface; use Chill\MainBundle\Templating\TranslatableStringHelper; +use Chill\PersonBundle\Entity\AccompanyingPeriod\UserHistory; use Chill\PersonBundle\Export\Declarations; use Doctrine\Common\Collections\Collection; -use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\FormBuilderInterface; -class UserJobFilter implements FilterInterface +final readonly class UserJobFilter implements FilterInterface, DataTransformerFilterInterface { private const PREFIX = 'acp_filter_user_job'; public function __construct( - private readonly TranslatableStringHelper $translatableStringHelper, - private readonly UserJobRepositoryInterface $userJobRepository, + private TranslatableStringHelper $translatableStringHelper, + private UserJobRepositoryInterface $userJobRepository, + private RollingDateConverterInterface $rollingDateConverter, ) { } @@ -42,42 +47,31 @@ class UserJobFilter implements FilterInterface { $p = self::PREFIX; - $qb - ->leftJoin( - 'acp.userHistories', - "{$p}_userHistory", - Join::WITH, - $qb->expr()->andX( - $qb->expr()->eq("{$p}_userHistory.accompanyingPeriod", 'acp.id'), - $qb->expr()->andX( - $qb->expr()->gte('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.startDate"), - $qb->expr()->orX( - $qb->expr()->isNull("{$p}_userHistory.endDate"), - $qb->expr()->lt('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.endDate") - ) - ) - ) - ) - ->leftJoin( - UserJobHistory::class, - "{$p}_jobHistory", - Join::WITH, - $qb->expr()->andX( - $qb->expr()->eq("{$p}_jobHistory.user", "{$p}_userHistory.user"), - $qb->expr()->andX( - $qb->expr()->lte("{$p}_jobHistory.startDate", "{$p}_userHistory.startDate"), - $qb->expr()->orX( - $qb->expr()->isNull("{$p}_jobHistory".'.endDate'), - $qb->expr()->gt("{$p}_jobHistory.endDate", "{$p}_userHistory.startDate") - ) - ) - ) - ) - ->andWhere($qb->expr()->in("{$p}_jobHistory.job", ":{$p}_job")) - ->setParameter( - "{$p}_job", - $data['jobs'], + $qb->andWhere( + $qb->expr()->exists( + sprintf( + << :{$p}_startDate) + AND {$p}_userJobHistory.startDate <= :{$p}_endDate + AND ({$p}_userJobHistory.endDate IS NULL OR {$p}_userJobHistory.endDate > :{$p}_startDate) + AND {$p}_userJobHistory.job IN (:{$p}_jobs) + DQL, + UserHistory::class, + UserJobHistory::class, + ), ) + ) + ->setParameter("{$p}_jobs", $data['jobs']) + ->setParameter("{$p}_startDate", $this->rollingDateConverter->convert($data['start_date'])) + ->setParameter("{$p}_endDate", $this->rollingDateConverter->convert($data['end_date'])) ; } @@ -96,20 +90,29 @@ class UserJobFilter implements FilterInterface 'expanded' => true, 'choice_label' => fn (UserJob $job) => $this->translatableStringHelper->localize($job->getLabel()), 'label' => 'Job', - ]); + ]) + ->add('start_date', PickRollingDateType::class, [ + 'label' => 'export.filter.course.by_user_job.Start from', + ]) + ->add('end_date', PickRollingDateType::class, [ + 'label' => 'export.filter.course.by_user_job.Until', + ]) + ; } public function describeAction($data, $format = 'string') { return [ - 'export.filter.course.by_user_job.Filtered by user job: only %job%', [ - '%job%' => implode( + 'exports.filter.course.by_user_job.Filtered by user job: only job', [ + 'job' => implode( ', ', array_map( fn (UserJob $job) => $this->translatableStringHelper->localize($job->getLabel()), $data['jobs'] instanceof Collection ? $data['jobs']->toArray() : $data['jobs'] ) ), + 'startDate' => $this->rollingDateConverter->convert($data['start_date']), + 'endDate' => $this->rollingDateConverter->convert($data['end_date']), ], ]; } @@ -118,9 +121,30 @@ class UserJobFilter implements FilterInterface { return [ 'jobs' => [], + 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), + 'end_date' => new RollingDate(RollingDate::T_TODAY), ]; } + public function transformData(?array $before): array + { + $default = $this->getFormDefaultData(); + + if (null === $before) { + return $default; + } + + if (!array_key_exists('start_date', $before) || null === $before['start_date']) { + $before['start_date'] = $default['start_date']; + } + + if (!array_key_exists('end_date', $before) || null === $before['end_date']) { + $before['end_date'] = $default['end_date']; + } + + return $before; + } + public function getTitle(): string { return 'export.filter.course.by_user_job.Filter by user job'; diff --git a/src/Bundle/ChillPersonBundle/Tests/Export/Filter/AccompanyingCourseFilters/UserJobFilterTest.php b/src/Bundle/ChillPersonBundle/Tests/Export/Filter/AccompanyingCourseFilters/UserJobFilterTest.php index 560fe26a7..72ed60fb4 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Export/Filter/AccompanyingCourseFilters/UserJobFilterTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Export/Filter/AccompanyingCourseFilters/UserJobFilterTest.php @@ -50,12 +50,14 @@ final class UserJobFilterTest extends AbstractFilterTest yield [ 'jobs' => new ArrayCollection($jobs), - 'date_calc' => new RollingDate(RollingDate::T_TODAY), + 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), + 'end_date' => new RollingDate(RollingDate::T_TODAY), ]; yield [ 'jobs' => $jobs, - 'date_calc' => new RollingDate(RollingDate::T_TODAY), + 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), + 'end_date' => new RollingDate(RollingDate::T_TODAY), ]; } diff --git a/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml b/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml index a268ac6c4..d6727f7c1 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml +++ b/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml @@ -143,6 +143,8 @@ exports: by_referrer_between_dates: description: >- Filtré par référent du parcours, entre deux dates: depuis le {start_date, date, medium}, jusqu'au {end_date, date, medium}, seulement {agents} + by_user_job: + "Filtered by user job: only job": "Filtré par métier du référent entre le {startDate, date, short} et le {endDate, date, short}: uniquement {job}" work: by_treating_agent: Filtered by treating agent at date: >- diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index fbd33dfd2..d6c10b89d 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -1232,7 +1232,8 @@ export: 'Filtered by creator job: only %jobs%': "Filtré par métier du créateur: uniquement %jobs%" by_user_job: Filter by user job: Filtrer les parcours par métier du référent - "Filtered by user job: only %job%": "Filtré par métier du référent: uniquement %job%" + Start from: Référent et métier depuis le + Until: Jusqu'au by_social_action: title: Filtrer les parcours par action d'accompagnement Accepted socialactions: Actions d'accompagnement From fc8bc33ba998535e28e98969e99629a912a34025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 17 Jun 2024 14:10:41 +0200 Subject: [PATCH 03/10] Add startDate and endDate on UserScopeFilter --- .../unreleased/Feature-20240617-142505.yaml | 6 + .../UserScopeFilter.php | 107 +++++++++++------- .../UserScopeFilterTest.php | 6 +- .../translations/messages+intl-icu.fr.yaml | 2 + .../translations/messages.fr.yml | 3 +- 5 files changed, 78 insertions(+), 46 deletions(-) create mode 100644 .changes/unreleased/Feature-20240617-142505.yaml diff --git a/.changes/unreleased/Feature-20240617-142505.yaml b/.changes/unreleased/Feature-20240617-142505.yaml new file mode 100644 index 000000000..13a2ded4e --- /dev/null +++ b/.changes/unreleased/Feature-20240617-142505.yaml @@ -0,0 +1,6 @@ +kind: Feature +body: '[export] add start date and end date on filters "filter course by referrer + job" and "filter course by referrer scope"' +time: 2024-06-17T14:25:05.041546007+02:00 +custom: + Issue: "282" diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserScopeFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserScopeFilter.php index bfbe8e66d..7e6b1d185 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserScopeFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserScopeFilter.php @@ -14,22 +14,26 @@ namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User\UserScopeHistory; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Form\Type\PickRollingDateType; use Chill\MainBundle\Repository\ScopeRepositoryInterface; +use Chill\MainBundle\Service\RollingDate\RollingDate; +use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface; use Chill\MainBundle\Templating\TranslatableStringHelper; +use Chill\PersonBundle\Entity\AccompanyingPeriod\UserHistory; use Chill\PersonBundle\Export\Declarations; use Doctrine\Common\Collections\Collection; -use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\FormBuilderInterface; -class UserScopeFilter implements FilterInterface +final readonly class UserScopeFilter implements FilterInterface { private const PREFIX = 'acp_filter_main_scope'; public function __construct( - private readonly ScopeRepositoryInterface $scopeRepository, - private readonly TranslatableStringHelper $translatableStringHelper, + private ScopeRepositoryInterface $scopeRepository, + private TranslatableStringHelper $translatableStringHelper, + private RollingDateConverterInterface $rollingDateConverter, ) { } @@ -38,47 +42,35 @@ class UserScopeFilter implements FilterInterface return null; } - public function alterQuery(QueryBuilder $qb, $data) + public function alterQuery(QueryBuilder $qb, $data): void { $p = self::PREFIX; - $qb - ->join( - 'acp.userHistories', - "{$p}_userHistory", - Join::WITH, - $qb->expr()->andX( - $qb->expr()->eq("{$p}_userHistory.accompanyingPeriod", 'acp.id'), - $qb->expr()->andX( - $qb->expr()->gte('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.startDate"), - $qb->expr()->orX( - $qb->expr()->isNull("{$p}_userHistory.endDate"), - $qb->expr()->lt('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.endDate") - ) - ) - ) + $qb->andWhere( + $qb->expr()->exists( + sprintf( + << :{$p}_startDate) + AND {$p}_userScopeHistory.startDate <= :{$p}_endDate + AND ({$p}_userScopeHistory.endDate IS NULL OR {$p}_userScopeHistory.endDate > :{$p}_startDate) + AND {$p}_userScopeHistory.scope IN (:{$p}_scopes) + DQL, + UserHistory::class, + UserScopeHistory::class, + ), ) - ->join( - UserScopeHistory::class, - "{$p}_scopeHistory", - Join::WITH, - $qb->expr()->andX( - $qb->expr()->eq("{$p}_scopeHistory.user", "{$p}_userHistory.user"), - $qb->expr()->andX( - $qb->expr()->lte("{$p}_scopeHistory.startDate", "{$p}_userHistory.startDate"), - $qb->expr()->orX( - $qb->expr()->isNull("{$p}_scopeHistory.endDate"), - $qb->expr()->gt("{$p}_scopeHistory.endDate", "{$p}_userHistory.startDate") - ) - ) - ) - ) - ->andWhere($qb->expr()->in("{$p}_scopeHistory.scope", ":{$p}_scopes")) - ->setParameter( - "{$p}_scopes", - $data['scopes'], - ) - ; + ) + ->setParameter("{$p}_scopes", $data['scopes']) + ->setParameter("{$p}_startDate", $this->rollingDateConverter->convert($data['start_date'])) + ->setParameter("{$p}_endDate", $this->rollingDateConverter->convert($data['end_date'])); } public function applyOn(): string @@ -95,20 +87,28 @@ class UserScopeFilter implements FilterInterface 'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()), 'multiple' => true, 'expanded' => true, + ]) + ->add('start_date', PickRollingDateType::class, [ + 'label' => 'export.filter.course.by_user_scope.Start from', + ]) + ->add('end_date', PickRollingDateType::class, [ + 'label' => 'export.filter.course.by_user_scope.Until', ]); } public function describeAction($data, $format = 'string') { return [ - 'export.filter.course.by_user_scope.Filtered by user main scope: only %scope%', [ - '%scope%' => implode( + 'exports.filter.course.by_user_scope.Filtered by user main scope: only scopes', [ + 'scopes' => implode( ', ', array_map( fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()), $data['scopes'] instanceof Collection ? $data['scopes']->toArray() : $data['scopes'] ) ), + 'startDate' => $this->rollingDateConverter->convert($data['start_date']), + 'endDate' => $this->rollingDateConverter->convert($data['end_date']), ], ]; } @@ -117,9 +117,30 @@ class UserScopeFilter implements FilterInterface { return [ 'scopes' => [], + 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), + 'end_date' => new RollingDate(RollingDate::T_TODAY), ]; } + public function transformData(?array $before): array + { + $default = $this->getFormDefaultData(); + + if (null === $before) { + return $default; + } + + if (!array_key_exists('start_date', $before) || null === $before['start_date']) { + $before['start_date'] = $default['start_date']; + } + + if (!array_key_exists('end_date', $before) || null === $before['end_date']) { + $before['end_date'] = $default['end_date']; + } + + return $before; + } + public function getTitle(): string { return 'export.filter.course.by_user_scope.Filter by user scope'; diff --git a/src/Bundle/ChillPersonBundle/Tests/Export/Filter/AccompanyingCourseFilters/UserScopeFilterTest.php b/src/Bundle/ChillPersonBundle/Tests/Export/Filter/AccompanyingCourseFilters/UserScopeFilterTest.php index ec98f1bfd..c4d839ad3 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Export/Filter/AccompanyingCourseFilters/UserScopeFilterTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Export/Filter/AccompanyingCourseFilters/UserScopeFilterTest.php @@ -50,11 +50,13 @@ final class UserScopeFilterTest extends AbstractFilterTest return [ [ - 'date_calc' => new RollingDate(RollingDate::T_TODAY), + 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), + 'end_date' => new RollingDate(RollingDate::T_TODAY), 'scopes' => new ArrayCollection($scopes), ], [ - 'date_calc' => new RollingDate(RollingDate::T_TODAY), + 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), + 'end_date' => new RollingDate(RollingDate::T_TODAY), 'scopes' => $scopes, ], ]; diff --git a/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml b/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml index d6727f7c1..fdc79e4a0 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml +++ b/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml @@ -145,6 +145,8 @@ exports: Filtré par référent du parcours, entre deux dates: depuis le {start_date, date, medium}, jusqu'au {end_date, date, medium}, seulement {agents} by_user_job: "Filtered by user job: only job": "Filtré par métier du référent entre le {startDate, date, short} et le {endDate, date, short}: uniquement {job}" + by_user_scope: + "Filtered by user main scope: only scopes": "Filtré par service du référent entre le {startDate, date, short} et le {endDate, date, short}: uniquement {scopes}" work: by_treating_agent: Filtered by treating agent at date: >- diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index d6c10b89d..bb40df9a9 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -1215,7 +1215,8 @@ export: 'Filtered by steps: only %step% and between %date_from% and %date_to%': 'Filtré par statut: seulement %step%, entre %date_from% et %date_to%' by_user_scope: Filter by user scope: Filtrer les parcours par service du référent - "Filtered by user main scope: only %scope%": "Filtré par service du référent: uniquement %scope%" + Start from: Référent et service depuis le + Until: Jusqu'au by_referrer: Computation date for referrer: Date à laquelle le référent était actif by_referrer_between_dates: From e7ca89e0c1b35d068916d572c1032f438ed3f2b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 17 Jun 2024 15:20:54 +0200 Subject: [PATCH 04/10] Rename DataTransformerFilterInterface to DataTransformerInterface The DataTransformerFilterInterface has been renamed to DataTransformerInterface to reflect expanded functionality. Now, this interface can be implemented not only by @see{FilterInterface}, but also by @see{AggregatorInterface}. This change allows transforming existing data in saved exports and replacing it with some default values, or new default values. --- ...rface.php => DataTransformerInterface.php} | 6 ++--- .../Form/Type/Export/AggregatorType.php | 25 ++++++++++++------- .../Form/Type/Export/FilterType.php | 4 +-- .../UserJobFilter.php | 4 +-- .../UserScopeFilter.php | 3 ++- 5 files changed, 25 insertions(+), 17 deletions(-) rename src/Bundle/ChillMainBundle/Export/{DataTransformerFilterInterface.php => DataTransformerInterface.php} (64%) diff --git a/src/Bundle/ChillMainBundle/Export/DataTransformerFilterInterface.php b/src/Bundle/ChillMainBundle/Export/DataTransformerInterface.php similarity index 64% rename from src/Bundle/ChillMainBundle/Export/DataTransformerFilterInterface.php rename to src/Bundle/ChillMainBundle/Export/DataTransformerInterface.php index 055ee2edc..ed34bc9d6 100644 --- a/src/Bundle/ChillMainBundle/Export/DataTransformerFilterInterface.php +++ b/src/Bundle/ChillMainBundle/Export/DataTransformerInterface.php @@ -16,10 +16,10 @@ namespace Chill\MainBundle\Export; * * This interface defines a method for transforming filter's form data before it is processed. * - * You can implement this interface on @see{FilterInterface}, to allow to transform existing data in saved exports - * and replace it with some default values. + * You can implement this interface on @see{FilterInterface} or @see{AggregatorInterface}, to allow to transform existing data in saved exports + * and replace it with some default values, or new default values. */ -interface DataTransformerFilterInterface +interface DataTransformerInterface { public function transformData(?array $before): array; } diff --git a/src/Bundle/ChillMainBundle/Form/Type/Export/AggregatorType.php b/src/Bundle/ChillMainBundle/Form/Type/Export/AggregatorType.php index 1ea01d5f8..89d4586ce 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Export/AggregatorType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Export/AggregatorType.php @@ -11,7 +11,9 @@ declare(strict_types=1); namespace Chill\MainBundle\Form\Type\Export; +use Chill\MainBundle\Export\DataTransformerInterface; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\FormBuilderInterface; @@ -19,11 +21,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class AggregatorType extends AbstractType { - public function __construct() - { - } - - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $exportManager = $options['export_manager']; $aggregator = $exportManager->getAggregator($options['aggregator_alias']); @@ -34,17 +32,26 @@ class AggregatorType extends AbstractType 'required' => false, ]); - $filterFormBuilder = $builder->create('form', FormType::class, [ + $aggregatorFormBuilder = $builder->create('form', FormType::class, [ 'compound' => true, 'required' => false, 'error_bubbling' => false, ]); - $aggregator->buildForm($filterFormBuilder); + $aggregator->buildForm($aggregatorFormBuilder); - $builder->add($filterFormBuilder); + if ($aggregator instanceof DataTransformerInterface) { + $aggregatorFormBuilder->addViewTransformer(new CallbackTransformer( + fn (?array $data) => $data, + function (?array $data) use ($aggregator) { + return $aggregator->transformData($data); + }, + )); + } + + $builder->add($aggregatorFormBuilder); } - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired('aggregator_alias') ->setRequired('export_manager') diff --git a/src/Bundle/ChillMainBundle/Form/Type/Export/FilterType.php b/src/Bundle/ChillMainBundle/Form/Type/Export/FilterType.php index 4e0976c12..5ac876bfe 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Export/FilterType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Export/FilterType.php @@ -11,7 +11,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Form\Type\Export; -use Chill\MainBundle\Export\DataTransformerFilterInterface; +use Chill\MainBundle\Export\DataTransformerInterface; use Chill\MainBundle\Export\FilterInterface; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\CallbackTransformer; @@ -45,7 +45,7 @@ class FilterType extends AbstractType ]); $filter->buildForm($filterFormBuilder); - if ($filter instanceof DataTransformerFilterInterface) { + if ($filter instanceof DataTransformerInterface) { $filterFormBuilder->addViewTransformer(new CallbackTransformer( fn (?array $data) => $data, function (?array $data) use ($filter) { diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserJobFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserJobFilter.php index 120d0a70e..c958272fc 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserJobFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserJobFilter.php @@ -13,7 +13,7 @@ namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters; use Chill\MainBundle\Entity\User\UserJobHistory; use Chill\MainBundle\Entity\UserJob; -use Chill\MainBundle\Export\DataTransformerFilterInterface; +use Chill\MainBundle\Export\DataTransformerInterface; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Form\Type\PickRollingDateType; use Chill\MainBundle\Repository\UserJobRepositoryInterface; @@ -27,7 +27,7 @@ use Doctrine\ORM\QueryBuilder; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\FormBuilderInterface; -final readonly class UserJobFilter implements FilterInterface, DataTransformerFilterInterface +final readonly class UserJobFilter implements FilterInterface, DataTransformerInterface { private const PREFIX = 'acp_filter_user_job'; diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserScopeFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserScopeFilter.php index 7e6b1d185..5b8ff7526 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserScopeFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserScopeFilter.php @@ -13,6 +13,7 @@ namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User\UserScopeHistory; +use Chill\MainBundle\Export\DataTransformerInterface; use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Form\Type\PickRollingDateType; use Chill\MainBundle\Repository\ScopeRepositoryInterface; @@ -26,7 +27,7 @@ use Doctrine\ORM\QueryBuilder; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\FormBuilderInterface; -final readonly class UserScopeFilter implements FilterInterface +final readonly class UserScopeFilter implements FilterInterface, DataTransformerInterface { private const PREFIX = 'acp_filter_main_scope'; From 68d21c9267cde26939d890d8149aaa7d7b8703b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 17 Jun 2024 15:22:28 +0200 Subject: [PATCH 05/10] Update ReferrerAggregator to specify a date range as parameter The ReferrerAggregator in ChillPersonBundle has been updated to include start and end dates, replacing the previous single computation date. This provides greater flexibility in setting the timeframe for referrer data. The messages.fr.yml file has also been updated to reflect these changes. Relevant tests have been updated to match the new functionality. --- .../unreleased/Feature-20240617-152219.yaml | 5 ++ .../ReferrerAggregator.php | 48 ++++++++++++++----- .../ReferrerAggregatorTest.php | 40 +++++++++++++++- .../translations/messages.fr.yml | 4 +- 4 files changed, 81 insertions(+), 16 deletions(-) create mode 100644 .changes/unreleased/Feature-20240617-152219.yaml diff --git a/.changes/unreleased/Feature-20240617-152219.yaml b/.changes/unreleased/Feature-20240617-152219.yaml new file mode 100644 index 000000000..cb5b93e0f --- /dev/null +++ b/.changes/unreleased/Feature-20240617-152219.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: '[export] the aggregator "Group by referrer" now accept a date range.' +time: 2024-06-17T15:22:19.030556768+02:00 +custom: + Issue: "282" diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerAggregator.php index 78349dd56..925918e83 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerAggregator.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators; use Chill\MainBundle\Export\AggregatorInterface; +use Chill\MainBundle\Export\DataTransformerInterface; use Chill\MainBundle\Form\Type\PickRollingDateType; use Chill\MainBundle\Repository\UserRepository; use Chill\MainBundle\Service\RollingDate\RollingDate; @@ -21,14 +22,17 @@ use Chill\PersonBundle\Export\Declarations; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; -final readonly class ReferrerAggregator implements AggregatorInterface +final readonly class ReferrerAggregator implements AggregatorInterface, DataTransformerInterface { private const A = 'acp_ref_agg_uhistory'; private const P = 'acp_ref_agg_date'; - public function __construct(private UserRepository $userRepository, private UserRender $userRender, private RollingDateConverterInterface $rollingDateConverter) - { + public function __construct( + private UserRepository $userRepository, + private UserRender $userRender, + private RollingDateConverterInterface $rollingDateConverter + ) { } public function addRole(): ?string @@ -46,18 +50,16 @@ final readonly class ReferrerAggregator implements AggregatorInterface $qb->expr()->orX( $qb->expr()->isNull(self::A), $qb->expr()->andX( - $qb->expr()->lte(self::A.'.startDate', ':'.self::P), + $qb->expr()->lt(self::A.'.startDate', ':'.self::P.'_end_date'), $qb->expr()->orX( $qb->expr()->isNull(self::A.'.endDate'), - $qb->expr()->gt(self::A.'.endDate', ':'.self::P) + $qb->expr()->gte(self::A.'.endDate', ':'.self::P.'_start_date') ) ) ) ) - ->setParameter( - self::P, - $this->rollingDateConverter->convert($data['date_calc']) - ); + ->setParameter(':'.self::P.'_end_date', $this->rollingDateConverter->convert($data['end_date'])) + ->setParameter(':'.self::P.'_start_date', $this->rollingDateConverter->convert($data['end_date'])); } public function applyOn(): string @@ -68,15 +70,37 @@ final readonly class ReferrerAggregator implements AggregatorInterface public function buildForm(FormBuilderInterface $builder) { $builder - ->add('date_calc', PickRollingDateType::class, [ - 'label' => 'export.aggregator.course.by_referrer.Computation date for referrer', + ->add('start_date', PickRollingDateType::class, [ + 'label' => 'export.aggregator.course.by_referrer.Referrer after', + 'required' => true, + ]) + ->add('end_date', PickRollingDateType::class, [ + 'label' => 'export.aggregator.course.by_referrer.Until', 'required' => true, ]); } public function getFormDefaultData(): array { - return ['date_calc' => new RollingDate(RollingDate::T_TODAY)]; + return [ + 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), + 'end_date' => new RollingDate(RollingDate::T_TODAY), + ]; + } + + public function transformData(?array $before): array + { + $default = $this->getFormDefaultData(); + $data = []; + + if (null === $before) { + return $default; + } + + $data['start_date'] = $before['date_calc'] ?? $before['start_date'] ?? $default['start_date']; + $data['end_date'] = $before['date_calc'] ?? $before['end_date'] ?? $default['end_date']; + + return $data; } public function getLabels($key, array $values, $data) diff --git a/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/ReferrerAggregatorTest.php b/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/ReferrerAggregatorTest.php index 8951b61eb..fade92c04 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/ReferrerAggregatorTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/ReferrerAggregatorTest.php @@ -33,7 +33,40 @@ final class ReferrerAggregatorTest extends AbstractAggregatorTest $this->aggregator = self::$container->get('chill.person.export.aggregator_referrer'); } - public function getAggregator() + /** + * @dataProvider provideBeforeData + */ + public function testDataTransformer(?array $before, array $expected): void + { + $actual = $this->getAggregator()->transformData($before); + + self::assertEqualsCanonicalizing(array_keys($expected), array_keys($actual)); + foreach (['start_date', 'end_date'] as $key) { + self::assertInstanceOf(RollingDate::class, $actual[$key]); + self::assertEquals($expected[$key]->getRoll(), $actual[$key]->getRoll(), "Check that the roll is the same for {$key}"); + } + } + + public function provideBeforeData(): iterable + { + yield [ + ['date_calc' => new RollingDate(RollingDate::T_TODAY)], + ['start_date' => new RollingDate(RollingDate::T_TODAY), 'end_date' => new RollingDate(RollingDate::T_TODAY)], + ]; + + yield [ + ['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)], + ['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)], + ]; + + yield [ + null, + // this is the default configuration + ['start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)], + ]; + } + + public function getAggregator(): ReferrerAggregator { return $this->aggregator; } @@ -41,7 +74,10 @@ final class ReferrerAggregatorTest extends AbstractAggregatorTest public function getFormData(): array { return [ - ['date_calc' => new RollingDate(RollingDate::T_TODAY)], + [ + 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), + 'end_date' => new RollingDate(RollingDate::T_TODAY), + ], ]; } diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index bb40df9a9..fc95da2ea 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -1058,9 +1058,9 @@ export: by-user: title: Grouper les parcours par usager participant header: Usager participant - by_referrer: - Computation date for referrer: Date à laquelle le référent était actif + Referrer after: Référent après le + Until: Jusqu'au by_user_scope: Group course by referrer's scope: Grouper les parcours par service du référent Referrer's scope: Service du référent de parcours From 6bd38f1a58b92f4b0b6d53ff75dceb542f1eb4df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 17 Jun 2024 15:31:33 +0200 Subject: [PATCH 06/10] Refactor assertions in AbstractAggregatorTest The conditional checks in the AbstractAggregatorTest have been simplified. Instead of a complex inline condition with multiple checks, the test now uses straightforward assertions. This makes the code cleaner and easier to understand. --- .../Test/Export/AbstractAggregatorTest.php | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Test/Export/AbstractAggregatorTest.php b/src/Bundle/ChillMainBundle/Test/Export/AbstractAggregatorTest.php index e66096c63..0a2faa3a2 100644 --- a/src/Bundle/ChillMainBundle/Test/Export/AbstractAggregatorTest.php +++ b/src/Bundle/ChillMainBundle/Test/Export/AbstractAggregatorTest.php @@ -338,15 +338,11 @@ abstract class AbstractAggregatorTest extends KernelTestCase .'is a string or an be converted to a string', $key) ); - $this->assertTrue( - // conditions - \is_string((string) \call_user_func($closure, '_header')) - && !empty(\call_user_func($closure, '_header')) - && '_header' !== \call_user_func($closure, '_header'), - // message - sprintf('Test that the callable return by `getLabels` for key %s ' - .'can provide an header', $key) - ); + $head = \call_user_func($closure, '_header'); + + self::assertIsString($head); + self::assertNotEquals('', $head); + self::assertNotEquals('_header', $head); } } From 791b3776c56abd8172915a8069830f0c38ef7cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 17 Jun 2024 16:21:28 +0200 Subject: [PATCH 07/10] Add date range filter to referrer scope aggregator A date range filter was added to the 'ReferrerScopeAggregator' class. This new feature allows users to filter courses by their referrer's scope based on a specified date range. In addition, relevant unit tests and translations were updated to support this new functionality. --- .../unreleased/Feature-20240617-162110.yaml | 5 +++ .../ReferrerScopeAggregator.php | 43 ++++++++++++++++++- .../ReferrerScopeAggregatorTest.php | 39 +++++++++++++++-- .../translations/messages.fr.yml | 3 ++ 4 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 .changes/unreleased/Feature-20240617-162110.yaml diff --git a/.changes/unreleased/Feature-20240617-162110.yaml b/.changes/unreleased/Feature-20240617-162110.yaml new file mode 100644 index 000000000..c6801b08e --- /dev/null +++ b/.changes/unreleased/Feature-20240617-162110.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: '[export] add date range on "group course by scope''s referrer"' +time: 2024-06-17T16:21:10.342069726+02:00 +custom: + Issue: "282" diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerScopeAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerScopeAggregator.php index b0a44bcd1..c183cff70 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerScopeAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ReferrerScopeAggregator.php @@ -13,20 +13,25 @@ namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators; use Chill\MainBundle\Entity\User\UserScopeHistory; use Chill\MainBundle\Export\AggregatorInterface; +use Chill\MainBundle\Export\DataTransformerInterface; +use Chill\MainBundle\Form\Type\PickRollingDateType; use Chill\MainBundle\Repository\ScopeRepositoryInterface; +use Chill\MainBundle\Service\RollingDate\RollingDate; +use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\PersonBundle\Export\Declarations; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; -readonly class ReferrerScopeAggregator implements AggregatorInterface +readonly class ReferrerScopeAggregator implements AggregatorInterface, DataTransformerInterface { private const PREFIX = 'acp_agg_referrer_scope'; public function __construct( private ScopeRepositoryInterface $scopeRepository, private TranslatableStringHelperInterface $translatableStringHelper, + private RollingDateConverterInterface $rollingDateConverter, ) { } @@ -47,11 +52,16 @@ readonly class ReferrerScopeAggregator implements AggregatorInterface $qb->expr()->andX( $qb->expr()->eq("{$p}_userHistory.accompanyingPeriod", 'acp.id'), $qb->expr()->andX( + // check that the user is referrer when the accompanying period is opened $qb->expr()->gte('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.startDate"), $qb->expr()->orX( $qb->expr()->isNull("{$p}_userHistory.endDate"), $qb->expr()->lt('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.endDate") ) + ), + $qb->expr()->andX( + "{$p}_userHistory.startDate <= :{$p}_endDate", + "COALESCE({$p}_userHistory.endDate, CURRENT_TIMESTAMP()) > :{$p}_startDate" ) ) ) @@ -67,9 +77,15 @@ readonly class ReferrerScopeAggregator implements AggregatorInterface $qb->expr()->isNull("{$p}_scopeHistory.endDate"), $qb->expr()->gt("{$p}_scopeHistory.endDate", "{$p}_userHistory.startDate") ) + ), + $qb->expr()->andX( + "{$p}_scopeHistory.startDate <= :{$p}_endDate", + "COALESCE({$p}_scopeHistory.endDate, CURRENT_TIMESTAMP()) > :{$p}_startDate" ) ) ) + ->setParameter("{$p}_startDate", $this->rollingDateConverter->convert($data['start_date'])) + ->setParameter("{$p}_endDate", $this->rollingDateConverter->convert($data['end_date'])) ->addSelect("IDENTITY({$p}_scopeHistory.scope) AS {$p}_select") ->addGroupBy("{$p}_select"); } @@ -81,11 +97,34 @@ readonly class ReferrerScopeAggregator implements AggregatorInterface public function buildForm(FormBuilderInterface $builder) { + $builder + ->add('start_date', PickRollingDateType::class, [ + 'label' => 'export.aggregator.course.by_referrer_scope.Referrer and scope after', + 'required' => true, + ]) + ->add('end_date', PickRollingDateType::class, [ + 'label' => 'export.aggregator.course.by_referrer_scope.Until', + 'required' => true, + ]); } public function getFormDefaultData(): array { - return []; + return [ + 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), + 'end_date' => new RollingDate(RollingDate::T_TODAY), + ]; + } + + public function transformData(?array $before): array + { + $default = $this->getFormDefaultData(); + $data = []; + + $data['start_date'] = $before['start_date'] ?? new RollingDate(RollingDate::T_FIXED_DATE, new \DateTimeImmutable('1970-01-01')); + $data['end_date'] = $before['end_date'] ?? $default['end_date']; + + return $data; } public function getLabels($key, array $values, $data) diff --git a/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/ReferrerScopeAggregatorTest.php b/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/ReferrerScopeAggregatorTest.php index 539b856ec..399a333b7 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/ReferrerScopeAggregatorTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/ReferrerScopeAggregatorTest.php @@ -14,6 +14,7 @@ namespace Export\Aggregator\AccompanyingCourseAggregators; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Repository\ScopeRepositoryInterface; use Chill\MainBundle\Service\RollingDate\RollingDate; +use Chill\MainBundle\Service\RollingDate\RollingDateConverter; use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Test\Export\AbstractAggregatorTest; @@ -48,16 +49,46 @@ final class ReferrerScopeAggregatorTest extends AbstractAggregatorTest return new ReferrerScopeAggregator( $scopeRepository->reveal(), $translatableStringHelper->reveal(), - $dateConverter->reveal() + new RollingDateConverter(), ); } public function getFormData() { return [ - [ - 'date_calc' => new RollingDate(RollingDate::T_TODAY), - ], + ['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)], + ]; + } + + /** + * @dataProvider provideBeforeData + */ + public function testDataTransformer(?array $before, array $expected): void + { + $actual = $this->getAggregator()->transformData($before); + + self::assertEqualsCanonicalizing(array_keys($expected), array_keys($actual)); + foreach (['start_date', 'end_date'] as $key) { + self::assertInstanceOf(RollingDate::class, $actual[$key]); + self::assertEquals($expected[$key]->getRoll(), $actual[$key]->getRoll(), "Check that the roll is the same for {$key}"); + } + } + + public function provideBeforeData(): iterable + { + yield [ + null, + ['start_date' => new RollingDate(RollingDate::T_FIXED_DATE, new \DateTimeImmutable('1970-01-01')), 'end_date' => new RollingDate(RollingDate::T_TODAY)], + ]; + + yield [ + [], + ['start_date' => new RollingDate(RollingDate::T_FIXED_DATE, new \DateTimeImmutable('1970-01-01')), 'end_date' => new RollingDate(RollingDate::T_TODAY)], + ]; + + yield [ + ['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)], + ['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)], ]; } diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index fc95da2ea..d3922de26 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -1061,6 +1061,9 @@ export: by_referrer: Referrer after: Référent après le Until: Jusqu'au + by_referrer_scope: + Referrer and scope after: Référent et service après le + Until: Jusqu'au by_user_scope: Group course by referrer's scope: Grouper les parcours par service du référent Referrer's scope: Service du référent de parcours From fab00f679c2e7b386f9cb45342b12cdc046a8fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 17 Jun 2024 17:16:02 +0200 Subject: [PATCH 08/10] Add date range to UserJobAggregator This update includes adding start_date and end_date to UserJobAggregator. This addition allows the selection of a date range in the export feature. Accompanying this change are associated translations and tests. --- .../unreleased/Feature-20240617-171414.yaml | 5 +++ .../UserJobAggregator.php | 44 +++++++++++++++++-- .../UserJobAggregatorTest.php | 38 ++++++++++++++-- .../translations/messages.fr.yml | 3 ++ 4 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 .changes/unreleased/Feature-20240617-171414.yaml diff --git a/.changes/unreleased/Feature-20240617-171414.yaml b/.changes/unreleased/Feature-20240617-171414.yaml new file mode 100644 index 000000000..e6559f61f --- /dev/null +++ b/.changes/unreleased/Feature-20240617-171414.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: '[export] add date range on "group course by jobs''s referrer"' +time: 2024-06-17T17:14:14.737439251+02:00 +custom: + Issue: "282" diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/UserJobAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/UserJobAggregator.php index 0bf536929..a46fdc5b4 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/UserJobAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/UserJobAggregator.php @@ -13,20 +13,25 @@ namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators; use Chill\MainBundle\Entity\User\UserJobHistory; use Chill\MainBundle\Export\AggregatorInterface; +use Chill\MainBundle\Export\DataTransformerInterface; +use Chill\MainBundle\Form\Type\PickRollingDateType; use Chill\MainBundle\Repository\UserJobRepository; +use Chill\MainBundle\Service\RollingDate\RollingDate; +use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface; use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\PersonBundle\Export\Declarations; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; -final readonly class UserJobAggregator implements AggregatorInterface +final readonly class UserJobAggregator implements AggregatorInterface, DataTransformerInterface { private const PREFIX = 'acp_agg_user_job'; public function __construct( private UserJobRepository $jobRepository, - private TranslatableStringHelper $translatableStringHelper + private TranslatableStringHelper $translatableStringHelper, + private RollingDateConverterInterface $rollingDateConverter, ) { } @@ -52,6 +57,10 @@ final readonly class UserJobAggregator implements AggregatorInterface $qb->expr()->isNull("{$p}_userHistory.endDate"), $qb->expr()->lt('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.endDate") ) + ), + $qb->expr()->andX( + "{$p}_userHistory.startDate <= :{$p}_endDate", + "COALESCE({$p}_userHistory.endDate, CURRENT_TIMESTAMP()) > :{$p}_startDate" ) ) ) @@ -67,9 +76,15 @@ final readonly class UserJobAggregator implements AggregatorInterface $qb->expr()->isNull("{$p}_jobHistory.endDate"), $qb->expr()->gt("{$p}_jobHistory.endDate", "{$p}_userHistory.startDate") ) + ), + $qb->expr()->andX( + "{$p}_jobHistory.startDate <= :{$p}_endDate", + "COALESCE({$p}_jobHistory.endDate, CURRENT_TIMESTAMP()) > :{$p}_startDate" ) ) ) + ->setParameter("{$p}_startDate", $this->rollingDateConverter->convert($data['start_date'])) + ->setParameter("{$p}_endDate", $this->rollingDateConverter->convert($data['end_date'])) ->addSelect("IDENTITY({$p}_jobHistory.job) AS {$p}_select") ->addGroupBy("{$p}_select"); } @@ -81,11 +96,34 @@ final readonly class UserJobAggregator implements AggregatorInterface public function buildForm(FormBuilderInterface $builder) { + $builder + ->add('start_date', PickRollingDateType::class, [ + 'label' => 'export.aggregator.course.by_referrer_job.Referrer and job after', + 'required' => true, + ]) + ->add('end_date', PickRollingDateType::class, [ + 'label' => 'export.aggregator.course.by_referrer_job.Until', + 'required' => true, + ]); } public function getFormDefaultData(): array { - return []; + return [ + 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), + 'end_date' => new RollingDate(RollingDate::T_TODAY), + ]; + } + + public function transformData(?array $before): array + { + $default = $this->getFormDefaultData(); + $data = []; + + $data['start_date'] = $before['start_date'] ?? new RollingDate(RollingDate::T_FIXED_DATE, new \DateTimeImmutable('1970-01-01')); + $data['end_date'] = $before['end_date'] ?? $default['end_date']; + + return $data; } public function getLabels($key, array $values, $data) diff --git a/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/UserJobAggregatorTest.php b/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/UserJobAggregatorTest.php index a62886152..d6bd6cc48 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/UserJobAggregatorTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Export/Aggregator/AccompanyingCourseAggregators/UserJobAggregatorTest.php @@ -33,7 +33,39 @@ final class UserJobAggregatorTest extends AbstractAggregatorTest $this->aggregator = self::$container->get('chill.person.export.aggregator_referrer_job'); } - public function getAggregator() + /** + * @dataProvider provideBeforeData + */ + public function testDataTransformer(?array $before, array $expected): void + { + $actual = $this->getAggregator()->transformData($before); + + self::assertEqualsCanonicalizing(array_keys($expected), array_keys($actual)); + foreach (['start_date', 'end_date'] as $key) { + self::assertInstanceOf(RollingDate::class, $actual[$key]); + self::assertEquals($expected[$key]->getRoll(), $actual[$key]->getRoll(), "Check that the roll is the same for {$key}"); + } + } + + public function provideBeforeData(): iterable + { + yield [ + null, + ['start_date' => new RollingDate(RollingDate::T_FIXED_DATE, new \DateTimeImmutable('1970-01-01')), 'end_date' => new RollingDate(RollingDate::T_TODAY)], + ]; + + yield [ + [], + ['start_date' => new RollingDate(RollingDate::T_FIXED_DATE, new \DateTimeImmutable('1970-01-01')), 'end_date' => new RollingDate(RollingDate::T_TODAY)], + ]; + + yield [ + ['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)], + ['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)], + ]; + } + + public function getAggregator(): UserJobAggregator { return $this->aggregator; } @@ -41,9 +73,7 @@ final class UserJobAggregatorTest extends AbstractAggregatorTest public function getFormData(): array { return [ - [ - 'job_at' => new RollingDate(RollingDate::T_FIXED_DATE, \DateTimeImmutable::createFromFormat('Y-m-d', '2020-01-01')), - ], + ['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)], ]; } diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index d3922de26..60f29acdf 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -1064,6 +1064,9 @@ export: by_referrer_scope: Referrer and scope after: Référent et service après le Until: Jusqu'au + by_referrer_job: + Referrer and job after: Référent et métier après le + Until: Jusqu'au by_user_scope: Group course by referrer's scope: Grouper les parcours par service du référent Referrer's scope: Service du référent de parcours From 19e34d5dc06ee06f1fc62f7f63a1bede0c7c3e60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 17 Jun 2024 17:28:29 +0200 Subject: [PATCH 09/10] PHP CS Fixer updated (3.57.2 -> v3.59.3) --- .../Tests/Form/Type/TranslatableActivityTypeTest.php | 2 +- .../Connector/MSGraph/RemoteEventConverter.php | 4 ++-- .../CompilerPass/ShortMessageCompilerPass.php | 2 +- src/Bundle/ChillMainBundle/Export/ExportManager.php | 4 ++-- .../ChillMainBundle/Search/Utils/ExtractDateFromPattern.php | 6 +++--- .../Search/Utils/ExtractPhonenumberFromPattern.php | 2 +- .../Tests/Controller/AccompanyingCourseControllerTest.php | 4 ++-- .../ChillWopiBundle/src/Resources/config/services.php | 4 ++-- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Bundle/ChillActivityBundle/Tests/Form/Type/TranslatableActivityTypeTest.php b/src/Bundle/ChillActivityBundle/Tests/Form/Type/TranslatableActivityTypeTest.php index f3163f664..49729b64c 100644 --- a/src/Bundle/ChillActivityBundle/Tests/Form/Type/TranslatableActivityTypeTest.php +++ b/src/Bundle/ChillActivityBundle/Tests/Form/Type/TranslatableActivityTypeTest.php @@ -60,7 +60,7 @@ final class TranslatableActivityTypeTest extends KernelTestCase $this->assertInstanceOf( ActivityType::class, $form->getData()['type'], - 'The data is an instance of Chill\\ActivityBundle\\Entity\\ActivityType' + 'The data is an instance of Chill\ActivityBundle\Entity\ActivityType' ); $this->assertEquals($type->getId(), $form->getData()['type']->getId()); diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteEventConverter.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteEventConverter.php index 94b488ddd..84797f6f1 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteEventConverter.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteEventConverter.php @@ -37,12 +37,12 @@ class RemoteEventConverter * valid when the remote string contains also a timezone, like in * lastModifiedDate. */ - final public const REMOTE_DATETIMEZONE_FORMAT = 'Y-m-d\\TH:i:s.u?P'; + final public const REMOTE_DATETIMEZONE_FORMAT = 'Y-m-d\TH:i:s.u?P'; /** * Same as above, but sometimes the date is expressed with only 6 milliseconds. */ - final public const REMOTE_DATETIMEZONE_FORMAT_ALT = 'Y-m-d\\TH:i:s.uP'; + final public const REMOTE_DATETIMEZONE_FORMAT_ALT = 'Y-m-d\TH:i:s.uP'; private const REMOTE_DATE_FORMAT = 'Y-m-d\TH:i:s.u0'; diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/ShortMessageCompilerPass.php b/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/ShortMessageCompilerPass.php index ebbfcee1b..9da9154b3 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/ShortMessageCompilerPass.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/ShortMessageCompilerPass.php @@ -43,7 +43,7 @@ class ShortMessageCompilerPass implements CompilerPassInterface $defaultTransporter = new Reference(NullShortMessageSender::class); } elseif ('ovh' === $dsn['scheme']) { if (!class_exists('\\'.\Ovh\Api::class)) { - throw new RuntimeException('Class \\Ovh\\Api not found'); + throw new RuntimeException('Class \Ovh\Api not found'); } foreach (['user', 'host', 'pass'] as $component) { diff --git a/src/Bundle/ChillMainBundle/Export/ExportManager.php b/src/Bundle/ChillMainBundle/Export/ExportManager.php index 9cb2a54ba..d4e87d449 100644 --- a/src/Bundle/ChillMainBundle/Export/ExportManager.php +++ b/src/Bundle/ChillMainBundle/Export/ExportManager.php @@ -190,7 +190,7 @@ class ExportManager // throw an error if the export require other modifier, which is // not allowed when the export return a `NativeQuery` if (\count($export->supportsModifiers()) > 0) { - throw new \LogicException("The export with alias `{$exportAlias}` return ".'a `\\Doctrine\\ORM\\NativeQuery` and supports modifiers, which is not allowed. Either the method `supportsModifiers` should return an empty array, or return a `Doctrine\\ORM\\QueryBuilder`'); + throw new \LogicException("The export with alias `{$exportAlias}` return ".'a `\Doctrine\ORM\NativeQuery` and supports modifiers, which is not allowed. Either the method `supportsModifiers` should return an empty array, or return a `Doctrine\ORM\QueryBuilder`'); } } elseif ($query instanceof QueryBuilder) { // handle filters @@ -203,7 +203,7 @@ class ExportManager 'dql' => $query->getDQL(), ]); } else { - throw new \UnexpectedValueException('The method `intiateQuery` should return a `\\Doctrine\\ORM\\NativeQuery` or a `Doctrine\\ORM\\QueryBuilder` object.'); + throw new \UnexpectedValueException('The method `intiateQuery` should return a `\Doctrine\ORM\NativeQuery` or a `Doctrine\ORM\QueryBuilder` object.'); } $result = $export->getResult($query, $data[ExportType::EXPORT_KEY]); diff --git a/src/Bundle/ChillMainBundle/Search/Utils/ExtractDateFromPattern.php b/src/Bundle/ChillMainBundle/Search/Utils/ExtractDateFromPattern.php index f858797f5..9cce3fa13 100644 --- a/src/Bundle/ChillMainBundle/Search/Utils/ExtractDateFromPattern.php +++ b/src/Bundle/ChillMainBundle/Search/Utils/ExtractDateFromPattern.php @@ -14,9 +14,9 @@ namespace Chill\MainBundle\Search\Utils; class ExtractDateFromPattern { private const DATE_PATTERN = [ - ['([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))', 'Y-m-d'], // 1981-05-12 - ['((0[1-9]|[12]\\d|3[01])\\/(0[1-9]|1[0-2])\\/([12]\\d{3}))', 'd/m/Y'], // 15/12/1980 - ['((0[1-9]|[12]\\d|3[01])-(0[1-9]|1[0-2])-([12]\\d{3}))', 'd-m-Y'], // 15/12/1980 + ['([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))', 'Y-m-d'], // 1981-05-12 + ['((0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([12]\d{3}))', 'd/m/Y'], // 15/12/1980 + ['((0[1-9]|[12]\d|3[01])-(0[1-9]|1[0-2])-([12]\d{3}))', 'd-m-Y'], // 15/12/1980 ]; public function extractDates(string $subject): SearchExtractionResult diff --git a/src/Bundle/ChillMainBundle/Search/Utils/ExtractPhonenumberFromPattern.php b/src/Bundle/ChillMainBundle/Search/Utils/ExtractPhonenumberFromPattern.php index 1823e462c..2be3d54db 100644 --- a/src/Bundle/ChillMainBundle/Search/Utils/ExtractPhonenumberFromPattern.php +++ b/src/Bundle/ChillMainBundle/Search/Utils/ExtractPhonenumberFromPattern.php @@ -16,7 +16,7 @@ use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; class ExtractPhonenumberFromPattern { - private const PATTERN = '([\\+]{0,1}[0-9\\ ]{5,})'; + private const PATTERN = '([\+]{0,1}[0-9\ ]{5,})'; private readonly string $defaultCarrierCode; diff --git a/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseControllerTest.php b/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseControllerTest.php index 7b6bdd43b..47f6502a3 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseControllerTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseControllerTest.php @@ -74,7 +74,7 @@ final class AccompanyingCourseControllerTest extends WebTestCase $this->assertResponseRedirects(); $location = $this->client->getResponse()->headers->get('Location'); - $this->assertEquals(1, \preg_match('|^\\/[^\\/]+\\/parcours/([\\d]+)/edit$|', (string) $location)); + $this->assertEquals(1, \preg_match('|^\/[^\/]+\/parcours/([\d]+)/edit$|', (string) $location)); } /** @@ -93,7 +93,7 @@ final class AccompanyingCourseControllerTest extends WebTestCase $location = $this->client->getResponse()->headers->get('Location'); $matches = []; - $this->assertEquals(1, \preg_match('|^\\/[^\\/]+\\/parcours/([\\d]+)/edit$|', (string) $location, $matches)); + $this->assertEquals(1, \preg_match('|^\/[^\/]+\/parcours/([\d]+)/edit$|', (string) $location, $matches)); $id = $matches[1]; $period = self::$container->get(EntityManagerInterface::class) diff --git a/src/Bundle/ChillWopiBundle/src/Resources/config/services.php b/src/Bundle/ChillWopiBundle/src/Resources/config/services.php index a962c9e67..950502bc3 100644 --- a/src/Bundle/ChillWopiBundle/src/Resources/config/services.php +++ b/src/Bundle/ChillWopiBundle/src/Resources/config/services.php @@ -33,10 +33,10 @@ return static function (ContainerConfigurator $container) { ->autoconfigure(); $services - ->load('Chill\\WopiBundle\\Service\\', __DIR__.'/../../Service'); + ->load('Chill\WopiBundle\Service\\', __DIR__.'/../../Service'); $services - ->load('Chill\\WopiBundle\\Controller\\', __DIR__.'/../../Controller') + ->load('Chill\WopiBundle\Controller\\', __DIR__.'/../../Controller') ->tag('controller.service_arguments'); $services From 1f4bef754dec9ea9233a887455ee468e538aba50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 17 Jun 2024 17:29:16 +0200 Subject: [PATCH 10/10] Refactor callback functions to arrow functions The callback functions used in the addViewTransformer method in FilterType.php and AggregatorType.php were replaced with shorter arrow functions. This change was made to increase code readability and encourage consistency throughout the codebase. --- .../ChillMainBundle/Form/Type/Export/AggregatorType.php | 4 +--- src/Bundle/ChillMainBundle/Form/Type/Export/FilterType.php | 4 +--- .../Tests/DependencyInjection/ChillReportExtensionTest.php | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Form/Type/Export/AggregatorType.php b/src/Bundle/ChillMainBundle/Form/Type/Export/AggregatorType.php index 89d4586ce..72e501108 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Export/AggregatorType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Export/AggregatorType.php @@ -42,9 +42,7 @@ class AggregatorType extends AbstractType if ($aggregator instanceof DataTransformerInterface) { $aggregatorFormBuilder->addViewTransformer(new CallbackTransformer( fn (?array $data) => $data, - function (?array $data) use ($aggregator) { - return $aggregator->transformData($data); - }, + fn (?array $data) => $aggregator->transformData($data), )); } diff --git a/src/Bundle/ChillMainBundle/Form/Type/Export/FilterType.php b/src/Bundle/ChillMainBundle/Form/Type/Export/FilterType.php index 5ac876bfe..a968f4aa2 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Export/FilterType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Export/FilterType.php @@ -48,9 +48,7 @@ class FilterType extends AbstractType if ($filter instanceof DataTransformerInterface) { $filterFormBuilder->addViewTransformer(new CallbackTransformer( fn (?array $data) => $data, - function (?array $data) use ($filter) { - return $filter->transformData($data); - }, + fn (?array $data) => $filter->transformData($data), )); } diff --git a/src/Bundle/ChillReportBundle/Tests/DependencyInjection/ChillReportExtensionTest.php b/src/Bundle/ChillReportBundle/Tests/DependencyInjection/ChillReportExtensionTest.php index ae01d8d90..db938f0bf 100644 --- a/src/Bundle/ChillReportBundle/Tests/DependencyInjection/ChillReportExtensionTest.php +++ b/src/Bundle/ChillReportBundle/Tests/DependencyInjection/ChillReportExtensionTest.php @@ -41,7 +41,7 @@ final class ChillReportExtensionTest extends KernelTestCase } if (!$reportFounded) { - throw new \Exception('Class Chill\\ReportBundle\\Entity\\Report not found in chill_custom_fields.customizables_entities', 1); + throw new \Exception('Class Chill\ReportBundle\Entity\Report not found in chill_custom_fields.customizables_entities', 1); } } }