From f19b939bd429dde6f7f502cee896324f71f97ae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 27 Jun 2023 11:04:22 +0200 Subject: [PATCH 01/50] Fixed: rights on the action list in accompanying period main's page Add is_granted check on the action: - if update action is allowed, open in update mode; - if see action is allowed, open in see mode; - fallback to an inactive link (should not happens) --- .../unreleased/Fixed-20230627-110233.yaml | 6 +++++ ...st_recent_by_accompanying_period.html.twig | 26 ++++++++++--------- 2 files changed, 20 insertions(+), 12 deletions(-) create mode 100644 .changes/unreleased/Fixed-20230627-110233.yaml diff --git a/.changes/unreleased/Fixed-20230627-110233.yaml b/.changes/unreleased/Fixed-20230627-110233.yaml new file mode 100644 index 000000000..58bb23933 --- /dev/null +++ b/.changes/unreleased/Fixed-20230627-110233.yaml @@ -0,0 +1,6 @@ +kind: Fixed +body: On the accompanying course page, open the action on view mode if the user does + not have right to update them (i.e. if the accompanying period is closed) +time: 2023-06-27T11:02:33.027807027+02:00 +custom: + Issue: "116" diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/list_recent_by_accompanying_period.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/list_recent_by_accompanying_period.html.twig index 92b2e3920..897f19b46 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/list_recent_by_accompanying_period.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/list_recent_by_accompanying_period.html.twig @@ -1,9 +1,11 @@ -
+ +
From 4a5ac170ba30f548edaee801f08395ffa086cda0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 29 Jun 2023 13:16:40 +0200 Subject: [PATCH 02/50] [export] add dates for filter "user working on course" --- .../unreleased/Feature-20230629-131558.yaml | 6 +++ .../UserWorkingOnCourseFilter.php | 40 ++++++++++++++----- .../translations/messages.fr.yml | 4 +- 3 files changed, 40 insertions(+), 10 deletions(-) create mode 100644 .changes/unreleased/Feature-20230629-131558.yaml diff --git a/.changes/unreleased/Feature-20230629-131558.yaml b/.changes/unreleased/Feature-20230629-131558.yaml new file mode 100644 index 000000000..42d731c4f --- /dev/null +++ b/.changes/unreleased/Feature-20230629-131558.yaml @@ -0,0 +1,6 @@ +kind: Feature +body: '[export] on "filter by user working" on accompanying period, add two dates + to filters intervention within a period' +time: 2023-06-29T13:15:58.070316708+02:00 +custom: + Issue: "113" diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserWorkingOnCourseFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserWorkingOnCourseFilter.php index d078443af..1f9bfc61a 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserWorkingOnCourseFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/UserWorkingOnCourseFilter.php @@ -13,7 +13,10 @@ namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Form\Type\PickRollingDateType; use Chill\MainBundle\Form\Type\PickUserDynamicType; +use Chill\MainBundle\Service\RollingDate\RollingDate; +use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface; use Chill\MainBundle\Templating\Entity\UserRender; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Export\Declarations; @@ -27,11 +30,9 @@ use Symfony\Component\Form\FormBuilderInterface; */ readonly class UserWorkingOnCourseFilter implements FilterInterface { - private const AI_ALIAS = 'user_working_on_course_filter_acc_info'; - private const AI_USERS = 'user_working_on_course_filter_users'; - public function __construct( private UserRender $userRender, + private RollingDateConverterInterface $rollingDateConverter, ) { } @@ -40,11 +41,23 @@ readonly class UserWorkingOnCourseFilter implements FilterInterface $builder ->add('users', PickUserDynamicType::class, [ 'multiple' => true, - ]); + ]) + ->add('start_date', PickRollingDateType::class, [ + 'label' => 'export.filter.course.by_user_working.User working after' + ]) + ->add('end_date', PickRollingDateType::class, [ + 'label' => 'export.filter.course.by_user_working.User working before' + ]) + ; } + public function getFormDefaultData(): array { - return []; + return [ + 'users' => [], + 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), + 'end_date' => new RollingDate(RollingDate::T_TODAY), + ]; } public function getTitle(): string @@ -55,7 +68,7 @@ readonly class UserWorkingOnCourseFilter implements FilterInterface public function describeAction($data, $format = 'string'): array { return [ - 'export.filter.course.by_user_working.Filtered by user working on course: only %users%', [ + 'export.filter.course.by_user_working.Filtered by user working on course: only %users%, between %start_date% and %end_date%', [ '%users%' => implode( ', ', array_map( @@ -63,6 +76,8 @@ readonly class UserWorkingOnCourseFilter implements FilterInterface $data['users'] ) ), + '%start_date%' => $this->rollingDateConverter->convert($data['start_date'])?->format('d-m-Y'), + '%end_date%' => $this->rollingDateConverter->convert($data['end_date'])?->format('d-m-Y'), ], ]; } @@ -74,14 +89,21 @@ readonly class UserWorkingOnCourseFilter implements FilterInterface public function alterQuery(QueryBuilder $qb, $data): void { + $ai_alias = 'user_working_on_course_filter_acc_info'; + $ai_users = 'user_working_on_course_filter_users'; + $start = 'acp_use_work_on_start'; + $end = 'acp_use_work_on_end'; + $qb ->andWhere( $qb->expr()->exists( - "SELECT 1 FROM " . AccompanyingPeriod\AccompanyingPeriodInfo::class . " " . self::AI_ALIAS . " " . - "WHERE " . self::AI_ALIAS . ".user IN (:" . self::AI_USERS .") AND IDENTITY(" . self::AI_ALIAS . ".accompanyingPeriod) = acp.id" + "SELECT 1 FROM " . AccompanyingPeriod\AccompanyingPeriodInfo::class . " {$ai_alias} " . + "WHERE {$ai_alias}.user IN (:{$ai_users}) AND IDENTITY({$ai_alias}.accompanyingPeriod) = acp.id AND {$ai_alias}.infoDate >= :{$start} and {$ai_alias}.infoDate < :{$end}" ) ) - ->setParameter(self::AI_USERS, $data['users']) + ->setParameter($ai_users, $data['users']) + ->setParameter($start, $this->rollingDateConverter->convert($data['start_date'])) + ->setParameter($end, $this->rollingDateConverter->convert($data['end_date'])) ; } diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 01383c050..464e8fa67 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -1082,7 +1082,9 @@ export: Only course with events between %startDate% and %endDate%: Seulement les parcours ayant reçu une intervention entre le %startDate% et le %endDate% by_user_working: title: Filter les parcours par intervenant - 'Filtered by user working on course: only %users%': 'Filtré par intervenants sur le parcours: seulement %users%' + 'Filtered by user working on course: only %users%, between %start_date% and %end_date%': 'Filtré par intervenants sur le parcours: seulement %users%, entre le %start_date% et le %end_date%' + User working after: Intervention après le + User working before: Intervention avant le by_step: Filter by step: Filtrer les parcours par statut du parcours Filter by step between dates: Filtrer les parcours par statut du parcours entre deux dates From 393e59e22bdab368ca6ca53fea6e079a8f1a19ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 29 Jun 2023 16:00:50 +0200 Subject: [PATCH 03/50] DX: Rolling date: allow to receive a null parameter (RollingDate) When receiving a null parameter (a null rolling date), it will return null --- .changes/unreleased/DX-20230629-160029.yaml | 5 +++++ .../Service/RollingDate/RollingDateConverter.php | 6 +++++- .../Service/RollingDate/RollingDateConverterInterface.php | 6 +++++- 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 .changes/unreleased/DX-20230629-160029.yaml diff --git a/.changes/unreleased/DX-20230629-160029.yaml b/.changes/unreleased/DX-20230629-160029.yaml new file mode 100644 index 000000000..b83befec6 --- /dev/null +++ b/.changes/unreleased/DX-20230629-160029.yaml @@ -0,0 +1,5 @@ +kind: DX +body: 'Rolling Date: can receive a null parameter' +time: 2023-06-29T16:00:29.664814895+02:00 +custom: + Issue: "" diff --git a/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDateConverter.php b/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDateConverter.php index 026ff7a8a..72ad89c58 100644 --- a/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDateConverter.php +++ b/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDateConverter.php @@ -18,8 +18,12 @@ use UnexpectedValueException; class RollingDateConverter implements RollingDateConverterInterface { - public function convert(RollingDate $rollingDate): DateTimeImmutable + public function convert(?RollingDate $rollingDate): ?DateTimeImmutable { + if (null === $rollingDate) { + return null; + } + switch ($rollingDate->getRoll()) { case RollingDate::T_MONTH_CURRENT_START: return $this->toBeginOfMonth($rollingDate->getPivotDate()); diff --git a/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDateConverterInterface.php b/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDateConverterInterface.php index b20a5ced2..6c7d9a5bd 100644 --- a/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDateConverterInterface.php +++ b/src/Bundle/ChillMainBundle/Service/RollingDate/RollingDateConverterInterface.php @@ -15,5 +15,9 @@ use DateTimeImmutable; interface RollingDateConverterInterface { - public function convert(RollingDate $rollingDate): DateTimeImmutable; + /** + * @param RollingDate|null $rollingDate + * @return ($rollingDate is null ? null : DateTimeImmutable) + */ + public function convert(?RollingDate $rollingDate): ?DateTimeImmutable; } From 5a395b160fd990d423887a6451a15f1f52768f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 29 Jun 2023 17:39:26 +0200 Subject: [PATCH 04/50] [export] add aggregator and filter related to AccompanyingPeriodInfo + Center aggregator for Person see #113 --- .../unreleased/Feature-20230629-173445.yaml | 5 + .../unreleased/Feature-20230629-173509.yaml | 5 + .../unreleased/Feature-20230629-173544.yaml | 5 + .../unreleased/Feature-20230629-173617.yaml | 5 + .../unreleased/Feature-20230629-173822.yaml | 5 + .../unreleased/Feature-20230629-173844.yaml | 5 + exports_alias_conventions.md | 3 + .../JobWorkingOnCourseAggregator.php | 100 +++++++++++++ .../ScopeWorkingOnCourseAggregator.php | 101 ++++++++++++++ .../UserWorkingOnCourseAggregator.php | 100 +++++++++++++ .../PersonAggregators/CenterAggregator.php | 103 ++++++++++++++ .../JobWorkingOnCourseFilter.php | 130 +++++++++++++++++ .../OpenBetweenDatesFilter.php | 2 +- .../ScopeWorkingOnCourseFilter.php | 132 ++++++++++++++++++ .../services/exports_accompanying_course.yaml | 20 +++ .../config/services/exports_person.yaml | 5 + .../translations/messages.fr.yml | 28 +++- 17 files changed, 751 insertions(+), 3 deletions(-) create mode 100644 .changes/unreleased/Feature-20230629-173445.yaml create mode 100644 .changes/unreleased/Feature-20230629-173509.yaml create mode 100644 .changes/unreleased/Feature-20230629-173544.yaml create mode 100644 .changes/unreleased/Feature-20230629-173617.yaml create mode 100644 .changes/unreleased/Feature-20230629-173822.yaml create mode 100644 .changes/unreleased/Feature-20230629-173844.yaml create mode 100644 src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/JobWorkingOnCourseAggregator.php create mode 100644 src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ScopeWorkingOnCourseAggregator.php create mode 100644 src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/UserWorkingOnCourseAggregator.php create mode 100644 src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/CenterAggregator.php create mode 100644 src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/JobWorkingOnCourseFilter.php create mode 100644 src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ScopeWorkingOnCourseFilter.php diff --git a/.changes/unreleased/Feature-20230629-173445.yaml b/.changes/unreleased/Feature-20230629-173445.yaml new file mode 100644 index 000000000..2d3642a85 --- /dev/null +++ b/.changes/unreleased/Feature-20230629-173445.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: '[export] Add an aggregator by user''s job working on a course' +time: 2023-06-29T17:34:45.278993433+02:00 +custom: + Issue: "113" diff --git a/.changes/unreleased/Feature-20230629-173509.yaml b/.changes/unreleased/Feature-20230629-173509.yaml new file mode 100644 index 000000000..95fd23458 --- /dev/null +++ b/.changes/unreleased/Feature-20230629-173509.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: '[export] add an aggregator by user''s scope working on a course' +time: 2023-06-29T17:35:09.548758741+02:00 +custom: + Issue: "113" diff --git a/.changes/unreleased/Feature-20230629-173544.yaml b/.changes/unreleased/Feature-20230629-173544.yaml new file mode 100644 index 000000000..94e79bc6d --- /dev/null +++ b/.changes/unreleased/Feature-20230629-173544.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: '[export] on aggregator "user working on a course"' +time: 2023-06-29T17:35:44.998468724+02:00 +custom: + Issue: "" diff --git a/.changes/unreleased/Feature-20230629-173617.yaml b/.changes/unreleased/Feature-20230629-173617.yaml new file mode 100644 index 000000000..445b85cff --- /dev/null +++ b/.changes/unreleased/Feature-20230629-173617.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: '[export] add a center aggregator for Person' +time: 2023-06-29T17:36:17.635876613+02:00 +custom: + Issue: "113" diff --git a/.changes/unreleased/Feature-20230629-173822.yaml b/.changes/unreleased/Feature-20230629-173822.yaml new file mode 100644 index 000000000..6a1569d00 --- /dev/null +++ b/.changes/unreleased/Feature-20230629-173822.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: '[export] add a filter on "job working on a course"' +time: 2023-06-29T17:38:22.682951416+02:00 +custom: + Issue: "113" diff --git a/.changes/unreleased/Feature-20230629-173844.yaml b/.changes/unreleased/Feature-20230629-173844.yaml new file mode 100644 index 000000000..f307ebf57 --- /dev/null +++ b/.changes/unreleased/Feature-20230629-173844.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: '[export] Add a filter on "scope working on a course"' +time: 2023-06-29T17:38:44.238287822+02:00 +custom: + Issue: "113" diff --git a/exports_alias_conventions.md b/exports_alias_conventions.md index fd7844691..64df91030 100644 --- a/exports_alias_conventions.md +++ b/exports_alias_conventions.md @@ -18,6 +18,7 @@ These are alias conventions : | | SocialIssue::class | acp.socialIssues | acpsocialissue | | | User::class | acp.user | acpuser | | | AccompanyingPeriopStepHistory::class | acp.stepHistories | acpstephistories | +| | AccompanyingPeriodInfo::class | not existing (using custom WITH clause) | acpinfo | | AccompanyingPeriodWork::class | | | acpw | | | AccompanyingPeriodWorkEvaluation::class | acpw.accompanyingPeriodWorkEvaluations | workeval | | | User::class | acpw.referrers | acpwuser | @@ -28,6 +29,8 @@ These are alias conventions : | | Person::class | acppart.person | partperson | | AccompanyingPeriodWorkEvaluation::class | | | workeval | | | Evaluation::class | workeval.evaluation | eval | +| AccompanyingPeriodInfo::class | | | acpinfo | +| | User::class | acpinfo.user | acpinfo_user | | Goal::class | | | goal | | | Result::class | goal.results | goalresult | | Person::class | | | person | diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/JobWorkingOnCourseAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/JobWorkingOnCourseAggregator.php new file mode 100644 index 000000000..e93300e85 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/JobWorkingOnCourseAggregator.php @@ -0,0 +1,100 @@ +userJobRepository->find((int) $jobId)) { + return ''; + } + + return $this->translatableStringHelper->localize($job->getLabel()); + }; + } + + public function getQueryKeys($data) + { + return [self::COLUMN_NAME]; + } + + public function getTitle() + { + return 'export.aggregator.course.by_job_working.title'; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + if (!in_array('acpinfo', $qb->getAllAliases(), true)) { + $qb->leftJoin( + AccompanyingPeriodInfo::class, + 'acpinfo', + Join::WITH, + 'acp.id = IDENTITY(acpinfo.accompanyingPeriod)' + ); + } + + if (!in_array('acpinfo_user', $qb->getAllAliases(), true)) { + $qb->leftJoin('acpinfo.user', 'acpinfo_user'); + } + + $qb->addSelect('IDENTITY(acpinfo_user.userJob) AS ' . self::COLUMN_NAME); + $qb->addGroupBy(self::COLUMN_NAME); + } + + public function applyOn() + { + return Declarations::ACP_TYPE; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ScopeWorkingOnCourseAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ScopeWorkingOnCourseAggregator.php new file mode 100644 index 000000000..b9f493af9 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ScopeWorkingOnCourseAggregator.php @@ -0,0 +1,101 @@ +scopeRepository->find((int) $scopeId)) { + return ''; + } + + return $this->translatableStringHelper->localize($scope->getName()); + }; + } + + public function getQueryKeys($data) + { + return [self::COLUMN_NAME]; + } + + public function getTitle() + { + return 'export.aggregator.course.by_scope_working.title'; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + if (!in_array('acpinfo', $qb->getAllAliases(), true)) { + $qb->leftJoin( + AccompanyingPeriodInfo::class, + 'acpinfo', + Join::WITH, + 'acp.id = IDENTITY(acpinfo.accompanyingPeriod)' + ); + } + + if (!in_array('acpinfo_user', $qb->getAllAliases(), true)) { + $qb->leftJoin('acpinfo.user', 'acpinfo_user'); + } + + $qb->addSelect('IDENTITY(acpinfo_user.mainScope) AS ' . self::COLUMN_NAME); + $qb->addGroupBy(self::COLUMN_NAME); + } + + public function applyOn() + { + return Declarations::ACP_TYPE; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/UserWorkingOnCourseAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/UserWorkingOnCourseAggregator.php new file mode 100644 index 000000000..b4941fa01 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/UserWorkingOnCourseAggregator.php @@ -0,0 +1,100 @@ +userRepository->find((int) $userId)) { + return ''; + } + + return $this->userRender->renderString($user, []); + }; + } + + public function getQueryKeys($data) + { + return [self::COLUMN_NAME]; + } + + public function getTitle() + { + return 'export.aggregator.course.by_user_working.title'; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + if (!in_array('acpinfo', $qb->getAllAliases(), true)) { + $qb->leftJoin( + AccompanyingPeriodInfo::class, + 'acpinfo', + Join::WITH, + 'acp.id = IDENTITY(acpinfo.accompanyingPeriod)' + ); + } + + if (!in_array('acpinfo_user', $qb->getAllAliases(), true)) { + $qb->leftJoin('acpinfo.user', 'acpinfo_user'); + } + + $qb->addSelect('acpinfo_user.id AS ' . self::COLUMN_NAME); + $qb->addGroupBy('acpinfo_user.id'); + } + + public function applyOn() + { + return Declarations::ACP_TYPE; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/CenterAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/CenterAggregator.php new file mode 100644 index 000000000..9be0b0c7e --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/PersonAggregators/CenterAggregator.php @@ -0,0 +1,103 @@ +add('at_date', PickRollingDateType::class, [ + 'label' => 'export.aggregator.person.by_center.at_date', + ]); + } + + public function getFormDefaultData(): array + { + return [ + 'at_date' => new RollingDate(RollingDate::T_TODAY) + ]; + } + + public function getLabels($key, array $values, $data): Closure + { + return function (int|string|null $value) { + if (null === $value || '' === $value) { + return ''; + } + + if ('_header' === $value) { + return 'export.aggregator.person.by_center.center'; + } + + return (string) $this->centerRepository->find((int) $value)?->getName(); + }; + } + + public function getQueryKeys($data) + { + return [self::COLUMN_NAME]; + } + + public function getTitle() + { + return 'export.aggregator.person.by_center.title'; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $alias = 'pers_center_agg'; + $atDate = 'pers_center_agg_at_date'; + + $qb->leftJoin('person.centerHistory', $alias); + $qb + ->andWhere( + $qb->expr()->lte($alias.'.startDate', ':'.$atDate), + )->andWhere( + $qb->expr()->orX( + $qb->expr()->isNull($alias.'.endDate'), + $qb->expr()->gt($alias.'.endDate', ':'.$atDate) + ) + ); + $qb->setParameter($atDate, $this->rollingDateConverter->convert($data['at_date'])); + + $qb->addSelect("IDENTITY({$alias}.center) AS " . self::COLUMN_NAME); + $qb->addGroupBy(self::COLUMN_NAME); + } + + public function applyOn() + { + return Declarations::PERSON_TYPE; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/JobWorkingOnCourseFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/JobWorkingOnCourseFilter.php new file mode 100644 index 000000000..63a668b6d --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/JobWorkingOnCourseFilter.php @@ -0,0 +1,130 @@ +userJobRepository->findAllActive(); + usort($jobs, fn (UserJob $a, UserJob $b) => $this->translatableStringHelper->localize($a->getLabel()) <=> $this->translatableStringHelper->localize($b->getLabel())); + + $builder + ->add('jobs', EntityType::class, [ + 'class' => UserJob::class, + 'choices' => $jobs, + 'choice_label' => fn (UserJob $userJob) => $this->translatableStringHelper->localize($userJob->getLabel()), + 'multiple' => true, + 'expanded' => true, + ]) + ->add('start_date', PickRollingDateType::class, [ + 'label' => 'export.filter.course.by_job_working.Job working after' + ]) + ->add('end_date', PickRollingDateType::class, [ + 'label' => 'export.filter.course.by_job_working.Job working before' + ]) + ; + } + + public function getFormDefaultData(): array + { + return [ + 'jobs' => [], + 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), + 'end_date' => new RollingDate(RollingDate::T_TODAY), + ]; + } + + public function getTitle(): string + { + return 'export.filter.course.by_job_working.title'; + } + + public function describeAction($data, $format = 'string'): array + { + return [ + 'export.filter.course.by_job_working.Filtered by job working on course: only %jobs%, between %start_date% and %end_date%', [ + '%jobs%' => implode( + ', ', + array_map( + fn (UserJob $userJob) => $this->translatableStringHelper->localize($userJob->getLabel()), + $data['jobs'] + ) + ), + '%start_date%' => $this->rollingDateConverter->convert($data['start_date'])?->format('d-m-Y'), + '%end_date%' => $this->rollingDateConverter->convert($data['end_date'])?->format('d-m-Y'), + ], + ]; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data): void + { + $ai_alias = 'jobs_working_on_course_filter_acc_info'; + $ai_user_alias = 'jobs_working_on_course_filter_user'; + $ai_jobs = 'jobs_working_on_course_filter_jobs'; + $start = 'acp_jobs_work_on_start'; + $end = 'acp_jobs_work_on_end'; + + $qb + ->andWhere( + $qb->expr()->exists( + "SELECT 1 FROM " . AccompanyingPeriod\AccompanyingPeriodInfo::class . " {$ai_alias} JOIN {$ai_alias}.user {$ai_user_alias} " . + "WHERE IDENTITY({$ai_alias}.accompanyingPeriod) = acp.id + AND {$ai_user_alias}.userJob IN (:{$ai_jobs}) + AND {$ai_alias}.infoDate >= :{$start} and {$ai_alias}.infoDate < :{$end} + " + ) + ) + ->setParameter($ai_jobs, $data['jobs']) + ->setParameter($start, $this->rollingDateConverter->convert($data['start_date'])) + ->setParameter($end, $this->rollingDateConverter->convert($data['end_date'])) + ; + } + + public function applyOn(): string + { + return Declarations::ACP_TYPE; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/OpenBetweenDatesFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/OpenBetweenDatesFilter.php index e9413083f..69fdd7bc0 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/OpenBetweenDatesFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/OpenBetweenDatesFilter.php @@ -38,7 +38,7 @@ class OpenBetweenDatesFilter implements FilterInterface { $clause = $qb->expr()->andX( $qb->expr()->gte('acp.openingDate', ':datefrom'), - $qb->expr()->lte('acp.openingDate', ':dateto') + $qb->expr()->lt('acp.openingDate', ':dateto') ); $qb->andWhere($clause); diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ScopeWorkingOnCourseFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ScopeWorkingOnCourseFilter.php new file mode 100644 index 000000000..b9787bf52 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ScopeWorkingOnCourseFilter.php @@ -0,0 +1,132 @@ +scopeRepository->findAllActive(); + usort($scopes, fn (Scope $a, Scope $b) => $this->translatableStringHelper->localize($a->getName()) <=> $this->translatableStringHelper->localize($b->getName())); + + $builder + ->add('scopes', EntityType::class, [ + 'class' => Scope::class, + 'choices' => $scopes, + 'choice_label' => fn (Scope $scope) => $this->translatableStringHelper->localize($scope->getName()), + 'multiple' => true, + 'expanded' => true, + ]) + ->add('start_date', PickRollingDateType::class, [ + 'label' => 'export.filter.course.by_scope_working.Scope working after' + ]) + ->add('end_date', PickRollingDateType::class, [ + 'label' => 'export.filter.course.by_scope_working.Scope working before' + ]) + ; + } + + public function getFormDefaultData(): array + { + return [ + 'scopes' => [], + 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), + 'end_date' => new RollingDate(RollingDate::T_TODAY), + ]; + } + + public function getTitle(): string + { + return 'export.filter.course.by_scope_working.title'; + } + + public function describeAction($data, $format = 'string'): array + { + return [ + 'export.filter.course.by_scope_working.Filtered by scope working on course: only %scopes%, between %start_date% and %end_date%', [ + '%scopes%' => implode( + ', ', + array_map( + fn (Scope $scope) => $this->translatableStringHelper->localize($scope->getName()), + $data['scopes'] + ) + ), + '%start_date%' => $this->rollingDateConverter->convert($data['start_date'])?->format('d-m-Y'), + '%end_date%' => $this->rollingDateConverter->convert($data['end_date'])?->format('d-m-Y'), + ], + ]; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data): void + { + $ai_alias = 'scopes_working_on_course_filter_acc_info'; + $ai_user_alias = 'scopes_working_on_course_filter_user'; + $ai_scopes = 'scopes_working_on_course_filter_scopes'; + $start = 'acp_scopes_work_on_start'; + $end = 'acp_scopes_work_on_end'; + + $qb + ->andWhere( + $qb->expr()->exists( + "SELECT 1 FROM " . AccompanyingPeriod\AccompanyingPeriodInfo::class . " {$ai_alias} JOIN {$ai_alias}.user {$ai_user_alias} " . + "WHERE IDENTITY({$ai_alias}.accompanyingPeriod) = acp.id + AND {$ai_user_alias}.mainScope IN (:{$ai_scopes}) + AND {$ai_alias}.infoDate >= :{$start} and {$ai_alias}.infoDate < :{$end} + " + ) + ) + ->setParameter($ai_scopes, $data['scopes']) + ->setParameter($start, $this->rollingDateConverter->convert($data['start_date'])) + ->setParameter($end, $this->rollingDateConverter->convert($data['end_date'])) + ; + } + + public function applyOn(): string + { + return Declarations::ACP_TYPE; + } +} diff --git a/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml b/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml index c299bbaaa..c5a2dba68 100644 --- a/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml @@ -135,6 +135,14 @@ services: tags: - { name: chill.export_filter, alias: accompanyingcourse_user_working_on_filter } + Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\JobWorkingOnCourseFilter: + tags: + - { name: chill.export_filter, alias: accompanyingcourse_job_working_on_filter } + + Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\ScopeWorkingOnCourseFilter: + tags: + - { name: chill.export_filter, alias: accompanyingcourse_scope_working_on_filter } + Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\HavingAnAccompanyingPeriodInfoWithinDatesFilter: tags: - { name: chill.export_filter, alias: accompanyingcourse_info_within_filter } @@ -231,3 +239,15 @@ services: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\CreatorJobAggregator: tags: - { name: chill.export_aggregator, alias: accompanyingcourse_creator_job_aggregator } + + Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\UserWorkingOnCourseAggregator: + tags: + - { name: chill.export_aggregator, alias: accompanyingcourse_user_working_on_course_aggregator } + + Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\JobWorkingOnCourseAggregator: + tags: + - { name: chill.export_aggregator, alias: accompanyingcourse_job_working_on_course_aggregator } + + Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\ScopeWorkingOnCourseAggregator: + tags: + - { name: chill.export_aggregator, alias: accompanyingcourse_scope_working_on_course_aggregator } diff --git a/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml b/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml index bf372ea06..64360aee9 100644 --- a/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml @@ -177,3 +177,8 @@ services: tags: - { name: chill.export_aggregator, alias: person_household_compo_aggregator } + Chill\PersonBundle\Export\Aggregator\PersonAggregators\CenterAggregator: + tags: + - { name: chill.export_aggregator, alias: person_center_aggregator } + + diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 464e8fa67..2f7800e2b 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -372,7 +372,7 @@ Count people participating in an accompanying course by various parameters.: Com Exports of accompanying courses: Exports des parcours d'accompagnement Count accompanying courses: Nombre de parcours Count accompanying courses by various parameters: Compte le nombre de parcours en fonction de différents filtres. -Accompanying courses participation duration and number of participations: Durée moyenne et nombre des participation des usagers aux parcours +Accompanying courses participation duration and number of participations: Durée moyenne et nombre des participations des usagers aux parcours Create an average of accompanying courses duration of each person participation to accompanying course, according to filters on persons, accompanying course: Crée un rapport qui comptabilise la moyenne de la durée de participation de chaque usager concerné aux parcours, avec différents filtres, notamment sur les usagers concernés. Closingdate to apply: Date de fin à prendre en compte lorsque le parcours n'est pas clotûré @@ -1016,6 +1016,11 @@ export: Household composition: Composition du ménage Group course by household composition: Grouper les usagers par composition familiale Calc date: Date de calcul de la composition du ménage + by_center: + title: Grouper les usagers par centre + at_date: Date de calcul du centre + center: Centre de l'usager + course: by_referrer: Computation date for referrer: Date à laquelle le référent était actif @@ -1032,6 +1037,15 @@ export: Number of actions: Nombre d'actions by_creator_job: Creator's job: Métier du créateur + by_user_working: + title: Grouper les parcours par intervenant + user: Intervenant + by_job_working: + title: Grouper les parcours par métier de l'intervenant + job: Métier de l'intervenant + by_scope_working: + title: Grouper les parcours par service de l'intervenant + scope: Service de l'intervenant course_work: by_current_action: Current action ?: Action en cours ? @@ -1081,10 +1095,20 @@ export: end_date: Fin de la période Only course with events between %startDate% and %endDate%: Seulement les parcours ayant reçu une intervention entre le %startDate% et le %endDate% by_user_working: - title: Filter les parcours par intervenant + title: Filter les parcours par intervenant, entre deux dates 'Filtered by user working on course: only %users%, between %start_date% and %end_date%': 'Filtré par intervenants sur le parcours: seulement %users%, entre le %start_date% et le %end_date%' User working after: Intervention après le User working before: Intervention avant le + by_job_working: + title: Filtrer les parcours par métier de l'intervenant, entre deux dates + 'Filtered by job working on course: only %jobs%, between %start_date% and %end_date%': 'Filtré par métier des intervenants sur le parcours: seulement %jobs%, entre le %start_date% et le %end_date%' + Job working after: Intervention après le + Job working before: Intervention avant le + by_scope_working: + title: Filtrer les parcours par service de l'intervenant, entre deux dates + 'Filtered by scope working on course: only %scopes%, between %start_date% and %end_date%': 'Filtré par service des intervenants sur le parcours: seulement %scopes%, entre le %start_date% et le %end_date%' + Scope working after: Intervention après le + Scope working before: Intervention avant le by_step: Filter by step: Filtrer les parcours par statut du parcours Filter by step between dates: Filtrer les parcours par statut du parcours entre deux dates From cc0e832cc97167105ea096be185b8334806c2047 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 19 Jan 2023 12:47:04 +0100 Subject: [PATCH 05/50] FEATURE [voter][confidential] added right to see confidential periods --- .../Security/Authorization/AccompanyingPeriodVoter.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php index 709624b54..9b4976847 100644 --- a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php +++ b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php @@ -107,6 +107,11 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH */ public const TOGGLE_INTENSITY = 'CHILL_PERSON_ACCOMPANYING_PERIOD_TOGGLE_INTENSITY'; + /** + * Right to see confidential period even if not referrer + */ + public const SEE_CONFIDENTIAL = 'CHILL_PERSON_ACCOMPANYING_PERIOD_SEE_CONFIDENTIAL'; + private Security $security; private VoterHelperInterface $voterHelper; From 9ccc57bbcb394bed6b1fe1f13ff07fa9d9f27ba7 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Thu, 19 Jan 2023 12:48:00 +0100 Subject: [PATCH 06/50] FEATURE [config][voter] config set for relation between bulk_assign and see_confidential --- .../DependencyInjection/ChillPersonExtension.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php index 569dd1502..576e563bd 100644 --- a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php +++ b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php @@ -985,6 +985,7 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac ], AccompanyingPeriodVoter::REASSIGN_BULK => [ AccompanyingPeriodVoter::CONFIDENTIAL_CRUD, + AccompanyingPeriodVoter::SEE_CONFIDENTIAL, ], AccompanyingPeriodVoter::TOGGLE_CONFIDENTIAL => [ AccompanyingPeriodVoter::CONFIDENTIAL_CRUD, From b3d993165d27a3354adbe2c3a65b4e59ebd0ddfd Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 25 Jan 2023 15:10:33 +0100 Subject: [PATCH 07/50] FEATURE: [confidential][voter] bulk assign right should also give right to access confidential parcours --- .../Security/Authorization/AccompanyingPeriodVoter.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php index 9b4976847..436f51792 100644 --- a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php +++ b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php @@ -225,6 +225,10 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH return true; } +/* if ($this->voterHelper->voteOnAttribute(self::REASSIGN_BULK, null, $token)) { + return true; + }*/ + return $token->getUser() === $subject->getUser(); } } From a7dbdc2b9d9bb2a6d7dd8f12ddacb2fd145735aa Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Fri, 10 Feb 2023 19:15:09 +0100 Subject: [PATCH 08/50] FEATURE [voter][confidential] voter adapted. repository changes left to do --- .../ReassignAccompanyingPeriodController.php | 6 ++++-- .../DependencyInjection/ChillPersonExtension.php | 8 ++++---- .../Authorization/AccompanyingPeriodVoter.php | 16 +++------------- 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Controller/ReassignAccompanyingPeriodController.php b/src/Bundle/ChillPersonBundle/Controller/ReassignAccompanyingPeriodController.php index bafc4b1cb..fde9746d3 100644 --- a/src/Bundle/ChillPersonBundle/Controller/ReassignAccompanyingPeriodController.php +++ b/src/Bundle/ChillPersonBundle/Controller/ReassignAccompanyingPeriodController.php @@ -20,6 +20,7 @@ use Chill\MainBundle\Repository\UserRepository; use Chill\MainBundle\Templating\Entity\UserRender; use Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepositoryInterface; use Chill\PersonBundle\Repository\AccompanyingPeriodRepository; +use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\CallbackTransformer; @@ -30,6 +31,7 @@ use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Security; @@ -85,8 +87,8 @@ class ReassignAccompanyingPeriodController extends AbstractController */ public function listAction(Request $request): Response { - if (!$this->security->isGranted('ROLE_USER') || !$this->security->getUser() instanceof User) { - throw new AccessDeniedException(); + if (!$this->security->isGranted(AccompanyingPeriodVoter::REASSIGN_BULK)) { + throw new AccessDeniedHttpException('no right to reassign bulk'); } $form = $this->buildFilterForm(); diff --git a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php index 576e563bd..6fd434635 100644 --- a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php +++ b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php @@ -984,11 +984,11 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac AccompanyingPeriodVoter::DELETE, ], AccompanyingPeriodVoter::REASSIGN_BULK => [ - AccompanyingPeriodVoter::CONFIDENTIAL_CRUD, - AccompanyingPeriodVoter::SEE_CONFIDENTIAL, + AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, + AccompanyingPeriodVoter::TOGGLE_CONFIDENTIAL_ALL, ], - AccompanyingPeriodVoter::TOGGLE_CONFIDENTIAL => [ - AccompanyingPeriodVoter::CONFIDENTIAL_CRUD, + AccompanyingPeriodVoter::TOGGLE_CONFIDENTIAL_ALL => [ + AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, ], ], ]); diff --git a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php index 436f51792..3dd991501 100644 --- a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php +++ b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php @@ -42,11 +42,6 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH self::RE_OPEN_COURSE, ]; - /** - * Give the ability to see all confidential courses. - */ - public const CONFIDENTIAL_CRUD = 'CHILL_PERSON_ACCOMPANYING_PERIOD_CRUD_CONFIDENTIAL'; - public const CREATE = 'CHILL_PERSON_ACCOMPANYING_PERIOD_CREATE'; /** @@ -110,7 +105,7 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH /** * Right to see confidential period even if not referrer */ - public const SEE_CONFIDENTIAL = 'CHILL_PERSON_ACCOMPANYING_PERIOD_SEE_CONFIDENTIAL'; + public const SEE_CONFIDENTIAL_ALL = 'CHILL_PERSON_ACCOMPANYING_PERIOD_SEE_CONFIDENTIAL'; private Security $security; @@ -136,7 +131,6 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH return [ self::SEE, self::SEE_DETAILS, - self::CONFIDENTIAL_CRUD, self::CREATE, self::EDIT, self::DELETE, @@ -154,7 +148,7 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH public function getRolesWithoutScope(): array { - return [self::REASSIGN_BULK]; + return []; } protected function supports($attribute, $subject) @@ -221,14 +215,10 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH // if confidential, only the referent can see it if ($subject->isConfidential()) { - if ($this->voterHelper->voteOnAttribute(self::CONFIDENTIAL_CRUD, $subject, $token)) { + if ($this->voterHelper->voteOnAttribute(self::SEE_CONFIDENTIAL_ALL, $subject, $token)) { return true; } -/* if ($this->voterHelper->voteOnAttribute(self::REASSIGN_BULK, null, $token)) { - return true; - }*/ - return $token->getUser() === $subject->getUser(); } } From dd344aed527df6718ebed27fe089991b4862693e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 4 Jul 2023 15:59:39 +0200 Subject: [PATCH 09/50] Implements right "see confidential course" on method findByPerson Add unit tests for that --- .../Authorization/AuthorizationHelper.php | 4 - ...orizationHelperForCurrentUserInterface.php | 5 +- .../AuthorizationHelperInterface.php | 3 +- .../AccompanyingPeriodController.php | 8 +- .../AccompanyingPeriodACLAwareRepository.php | 119 ++++++---- ...nyingPeriodACLAwareRepositoryInterface.php | 10 +- ...companyingPeriodACLAwareRepositoryTest.php | 206 ++++++++++++++++++ 7 files changed, 306 insertions(+), 49 deletions(-) create mode 100644 src/Bundle/ChillPersonBundle/Tests/Repository/AccompanyingPeriodACLAwareRepositoryTest.php diff --git a/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php b/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php index 1105c9d8a..be884de94 100644 --- a/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php +++ b/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php @@ -195,10 +195,6 @@ class AuthorizationHelper implements AuthorizationHelperInterface /** * Return all reachable scope for a given user, center and role. - * - * @param Center|Center[] $center - * - * @return array|Scope[] */ public function getReachableScopes(UserInterface $user, string $role, Center|array $center): array { diff --git a/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelperForCurrentUserInterface.php b/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelperForCurrentUserInterface.php index f0d3f9fba..54e30c244 100644 --- a/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelperForCurrentUserInterface.php +++ b/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelperForCurrentUserInterface.php @@ -25,7 +25,8 @@ interface AuthorizationHelperForCurrentUserInterface public function getReachableCenters(string $role, ?Scope $scope = null): array; /** - * @param array|Center|Center[] $center + * @param list
|Center $center + * @return list */ - public function getReachableScopes(string $role, $center): array; + public function getReachableScopes(string $role, array|Center $center): array; } diff --git a/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelperInterface.php b/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelperInterface.php index 1176cf1fa..1dc9668ec 100644 --- a/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelperInterface.php +++ b/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelperInterface.php @@ -26,7 +26,8 @@ interface AuthorizationHelperInterface public function getReachableCenters(UserInterface $user, string $role, ?Scope $scope = null): array; /** - * @param Center|list
$center + * @param Center|array
$center + * @return list */ public function getReachableScopes(UserInterface $user, string $role, Center|array $center): array; } diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingPeriodController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingPeriodController.php index b32454387..8b4c0b27a 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingPeriodController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingPeriodController.php @@ -219,13 +219,13 @@ class AccompanyingPeriodController extends AbstractController ]); $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); - $accompanyingPeriodsRaw = $this->accompanyingPeriodACLAwareRepository - ->findByPerson($person, AccompanyingPeriodVoter::SEE); + $accompanyingPeriods = $this->accompanyingPeriodACLAwareRepository + ->findByPerson($person, AccompanyingPeriodVoter::SEE, ["openingDate" => "DESC", "id" => "DESC"]); - usort($accompanyingPeriodsRaw, static fn ($a, $b) => $b->getOpeningDate() > $a->getOpeningDate()); + //usort($accompanyingPeriodsRaw, static fn ($a, $b) => $b->getOpeningDate() <=> $a->getOpeningDate()); // filter visible or not visible - $accompanyingPeriods = array_filter($accompanyingPeriodsRaw, fn (AccompanyingPeriod $ap) => $this->isGranted(AccompanyingPeriodVoter::SEE, $ap)); + //$accompanyingPeriods = array_filter($accompanyingPeriodsRaw, fn (AccompanyingPeriod $ap) => $this->isGranted(AccompanyingPeriodVoter::SEE, $ap)); return $this->render('@ChillPerson/AccompanyingPeriod/list.html.twig', [ 'accompanying_periods' => $accompanyingPeriods, diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php index 0aaabd05f..104f49ec7 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php @@ -13,54 +13,51 @@ namespace Chill\PersonBundle\Repository; use Chill\MainBundle\Entity\Address; use Chill\MainBundle\Entity\Location; -use Chill\MainBundle\Entity\PostalCode; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\UserJob; -use Chill\MainBundle\Security\Authorization\AuthorizationHelper; -use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface; +use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface; +use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress; use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; use DateTime; - use DateTimeImmutable; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; +use Repository\AccompanyingPeriodACLAwareRepositoryTest; use Symfony\Component\Security\Core\Security; use function count; -final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodACLAwareRepositoryInterface +/** + * @see AccompanyingPeriodACLAwareRepositoryTest + */ +final readonly class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodACLAwareRepositoryInterface { private AccompanyingPeriodRepository $accompanyingPeriodRepository; - private AuthorizationHelper $authorizationHelper; + private AuthorizationHelperForCurrentUserInterface $authorizationHelper; - private CenterResolverDispatcherInterface $centerResolverDispatcher; + private CenterResolverManagerInterface $centerResolver; private Security $security; public function __construct( AccompanyingPeriodRepository $accompanyingPeriodRepository, Security $security, - AuthorizationHelper $authorizationHelper, - CenterResolverDispatcherInterface $centerResolverDispatcher + AuthorizationHelperForCurrentUserInterface $authorizationHelper, + CenterResolverManagerInterface $centerResolverDispatcher ) { $this->accompanyingPeriodRepository = $accompanyingPeriodRepository; $this->security = $security; $this->authorizationHelper = $authorizationHelper; - $this->centerResolverDispatcher = $centerResolverDispatcher; + $this->centerResolver = $centerResolverDispatcher; } - /** - * @param array|PostalCode[] - * - * @return QueryBuilder - */ - public function buildQueryOpenedAccompanyingCourseByUser(?User $user, array $postalCodes = []) + public function buildQueryOpenedAccompanyingCourseByUser(?User $user, array $postalCodes = []): QueryBuilder { $qb = $this->accompanyingPeriodRepository->createQueryBuilder('ap'); @@ -152,10 +149,14 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC ): array { $qb = $this->accompanyingPeriodRepository->createQueryBuilder('ap'); $scopes = $this->authorizationHelper - ->getReachableCircles( - $this->security->getUser(), + ->getReachableScopes( $role, - $this->centerResolverDispatcher->resolveCenter($person) + $this->centerResolver->resolveCenters($person) + ); + $scopesCanSeeConfidential = $this->authorizationHelper + ->getReachableScopes( + AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, + $this->centerResolver->resolveCenters($person) ); if (0 === count($scopes)) { @@ -165,12 +166,42 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC $qb ->join('ap.participations', 'participation') ->where($qb->expr()->eq('participation.person', ':person')) - ->andWhere( - $qb->expr()->orX( - 'ap.confidential = FALSE', - $qb->expr()->eq('ap.user', ':user') - ) - ) + ->setParameter('person', $person); + + $qb = $this->addACLClauses($qb, $scopes, $scopesCanSeeConfidential); + $qb = $this->addOrderLimitClauses($qb, $orderBy, $limit, $offset); + + return $qb->getQuery()->getResult(); + } + + public function addOrderLimitClauses(QueryBuilder $qb, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): QueryBuilder + { + if (null !== $orderBy) { + foreach ($orderBy as $field => $order) { + $qb->addOrderBy('ap.' . $field, $order); + } + } + + if (null !== $limit) { + $qb->setMaxResults($limit); + } + + if (null !== $offset) { + $qb->setFirstResult($offset); + } + + return $qb; + } + + /** + * @param QueryBuilder $qb where the accompanying period have the `ap` alias + * @param array $scopesCanSee + * @param array $scopesCanSeeConfidential + * @return QueryBuilder + */ + public function addACLClauses(QueryBuilder $qb, array $scopesCanSee, array $scopesCanSeeConfidential): QueryBuilder + { + $qb ->andWhere( $qb->expr()->orX( $qb->expr()->neq('ap.step', ':draft'), @@ -181,25 +212,44 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC ) ) ->setParameter('draft', AccompanyingPeriod::STEP_DRAFT) - ->setParameter('person', $person) ->setParameter('user', $this->security->getUser()) ->setParameter('creator', $this->security->getUser()); + // add join condition for scopes $orx = $qb->expr()->orX( + // even if the scope is not in one authorized, the user can see the course if it is in DRAFT state $qb->expr()->eq('ap.step', ':draft') ); - foreach ($scopes as $key => $scope) { - $orx->add($qb->expr()->orX( + foreach ($scopesCanSee as $key => $scope) { + // for each scope: + // - either the user is the referrer of the course + // - or the accompanying course is one of the reachable scopes + // - and the parcours is not confidential OR the user is the referrer OR the user can see the confidential course + + $orOnScope = $qb->expr()->orX( $qb->expr()->isMemberOf(':scope_' . $key, 'ap.scopes'), $qb->expr()->eq('ap.user', ':user') - )); + ); + + if (in_array($scope, $scopesCanSeeConfidential, true)) { + $orx->add($orOnScope); + } else { + // we must add a condition: the course is not confidential or the user is the referrer + $andXOnScope = $qb->expr()->andX( + $orOnScope, + $qb->expr()->orX( + 'ap.confidential = FALSE', + $qb->expr()->eq('ap.user', ':user') + ) + ); + $orx->add($andXOnScope); + } $qb->setParameter('scope_' . $key, $scope); - $qb->setParameter('user', $this->security->getUser()); } $qb->andWhere($orx); - return $qb->getQuery()->getResult(); + return $qb; } public function findByUnDispatched(array $jobs, array $services, array $administrativeLocations, ?int $limit = null, ?int $offset = null): array @@ -237,9 +287,6 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC return $qb->getQuery()->getResult(); } - /** - * @return array|AccompanyingPeriod[] - */ public function findByUserOpenedAccompanyingPeriod(?User $user, array $orderBy = [], int $limit = 0, int $offset = 50): array { if (null === $user) { @@ -261,7 +308,6 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC private function addACLByUnDispatched(QueryBuilder $qb): QueryBuilder { $centers = $this->authorizationHelper->getReachableCenters( - $this->security->getUser(), AccompanyingPeriodVoter::SEE ); @@ -273,8 +319,7 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC foreach ($centers as $key => $center) { $scopes = $this->authorizationHelper - ->getReachableCircles( - $this->security->getUser(), + ->getReachableScopes( AccompanyingPeriodVoter::SEE, $center ); diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepositoryInterface.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepositoryInterface.php index 0cca1a5f4..ff3d89783 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepositoryInterface.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepositoryInterface.php @@ -33,6 +33,9 @@ interface AccompanyingPeriodACLAwareRepositoryInterface public function countByUserOpenedAccompanyingPeriod(?User $user): int; + /** + * @return array + */ public function findByPerson( Person $person, string $role, @@ -45,14 +48,19 @@ interface AccompanyingPeriodACLAwareRepositoryInterface * @param array|UserJob[] $jobs if empty, does not take this argument into account * @param array|Scope[] $services if empty, does not take this argument into account * - * @return array|AccompanyingPeriod[] + * @return list */ public function findByUnDispatched(array $jobs, array $services, array $administrativeLocations, ?int $limit = null, ?int $offset = null): array; /** * @param array|PostalCode[] $postalCodes + * @return list */ public function findByUserAndPostalCodesOpenedAccompanyingPeriod(?User $user, array $postalCodes, array $orderBy = [], int $limit = 0, int $offset = 50): array; + /** + * @deprecated + * @return list + */ public function findByUserOpenedAccompanyingPeriod(?User $user, array $orderBy = [], int $limit = 0, int $offset = 50): array; } diff --git a/src/Bundle/ChillPersonBundle/Tests/Repository/AccompanyingPeriodACLAwareRepositoryTest.php b/src/Bundle/ChillPersonBundle/Tests/Repository/AccompanyingPeriodACLAwareRepositoryTest.php new file mode 100644 index 000000000..17c12fea5 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Repository/AccompanyingPeriodACLAwareRepositoryTest.php @@ -0,0 +1,206 @@ +accompanyingPeriodRepository = self::$container->get(AccompanyingPeriodRepository::class); + $this->centerResolverManager = self::$container->get(CenterResolverManagerInterface::class); + $this->entityManager = self::$container->get(EntityManagerInterface::class); + $this->scopeRepository = self::$container->get(ScopeRepositoryInterface::class); + $this->registry = self::$container->get(Registry::class); + + } + + public static function tearDownAfterClass(): void + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + $repository = self::$container->get(AccompanyingPeriodRepository::class); + + foreach (self::$periodsIdsToDelete as $id) { + if (null === $period = $repository->find($id)) { + throw new \RuntimeException("period not found while trying to delete it"); + } + + foreach ($period->getParticipations() as $participation) { + $em->remove($participation); + } + $em->remove($period); + } + + $em->flush(); + } + + + /** + * For testing this method, we mock the authorization helper to return different Scope that a user + * can see, or that a user can see confidential periods. + * + * @param array $scopeUserCanSee + * @param array $scopeUserCanSeeConfidential + * @param array $expectedPeriod + * @dataProvider provideDataForFindByPerson + */ + public function testFindByPersonTestUser(User $user, Person $person, array $scopeUserCanSee, array $scopeUserCanSeeConfidential, array $expectedPeriod, string $message): void + { + $security = $this->prophesize(Security::class); + $security->getUser()->willReturn($user); + + $authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class); + $authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE, Argument::any()) + ->willReturn($scopeUserCanSee); + $authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, Argument::any()) + ->willReturn($scopeUserCanSeeConfidential); + + $repository = new AccompanyingPeriodACLAwareRepository( + $this->accompanyingPeriodRepository, + $security->reveal(), + $authorizationHelper->reveal(), + $this->centerResolverManager + ); + + $actuals = $repository->findByPerson($person, AccompanyingPeriodVoter::SEE); + $expectedIds = array_map(fn (AccompanyingPeriod $period) => $period->getId(), $expectedPeriod); + + self::assertCount(count($expectedPeriod), $actuals, $message); + foreach ($actuals as $actual) { + self::assertContains($actual->getId(), $expectedIds); + } + } + + public function provideDataForFindByPerson(): iterable + { + $this->setUp(); + + if (null === $user = $this->entityManager->createQuery("SELECT u FROM " . User::class . " u")->setMaxResults(1)->getSingleResult()) { + throw new \RuntimeException("no user found"); + } + + if (null === $anotherUser = $this->entityManager->createQuery("SELECT u FROM " . User::class . " u WHERE u.id != :uid")->setParameter('uid', $user->getId()) + ->setMaxResults(1)->getSingleResult()) { + throw new \RuntimeException("no user found"); + } + + [$person, $anotherPerson, $person2, $person3] = $this->entityManager + ->createQuery("SELECT p FROM " . Person::class . " p WHERE SIZE(p.accompanyingPeriodParticipations) = 0") + ->setMaxResults(4) + ->getResult(); + + if (null === $person || null === $anotherPerson || null === $person2 || null === $person3) { + throw new \RuntimeException("no person found"); + } + + $scopes = $this->scopeRepository->findAll(); + + if (3 > count($scopes)) { + throw new \RuntimeException("not enough scopes for this test"); + } + $scopesCanSee = [ $scopes[0] ]; + $scopesGroup2 = [ $scopes[1] ]; + + // case: a period is in draft state + $period = $this->buildPeriod($person, $scopesCanSee, $user, false); + + yield [$user, $person, $scopesCanSee, [], [$period], "a user can see his period during draft state"]; + + // another user is not allowed to see this period, because it is in DRAFT state + yield [$anotherUser, $person, $scopesCanSee, [], [], "another user is not allowed to see the period of someone else in draft state"]; + + // the period is confirmed + $period = $this->buildPeriod($anotherPerson, $scopesCanSee, $user, true); + + // the other user can now see it + yield [$user, $anotherPerson, $scopesCanSee, [], [$period], "a user see his period when confirmed"]; + yield [$anotherUser, $anotherPerson, $scopesCanSee, [], [$period], "another user with required scopes is allowed to see the period when not draft"]; + yield [$anotherUser, $anotherPerson, $scopesGroup2, [], [], "another user without the required scopes is not allowed to see the period when not draft"]; + + // this period will be confidential + $period = $this->buildPeriod($person2, $scopesCanSee, $user, true); + $period->setConfidential(true)->setUser($user, true); + + yield [$user, $person2, $scopesCanSee, [], [$period], "a user see his period when confirmed and confidential with required scopes"]; + yield [$user, $person2, $scopesGroup2, [], [$period], "a user see his period when confirmed and confidential without required scopes"]; + yield [$anotherUser, $person2, $scopesCanSee, [], [], "a user don't see a confidential period, even if he has required scopes"]; + yield [$anotherUser, $person2, $scopesCanSee, $scopesCanSee, [$period], "a user see the period when confirmed and confidential if he has required scope to see the period"]; + + // period draft with creator = null + $period = $this->buildPeriod($person3, $scopesCanSee, null, false); + yield [$user, $person3, $scopesCanSee, [], [$period], "a user see a period when draft if no creator on the period"]; + $this->entityManager->flush(); + } + + /** + * @param Person $person + * @param array $scopes + * @return AccompanyingPeriod + */ + private function buildPeriod(Person $person, array $scopes, User|null $creator, bool $confirm): AccompanyingPeriod + { + $period = new AccompanyingPeriod(); + $period->addPerson($person); + if (null !== $creator) { + $period->setCreatedBy($creator); + } + + foreach ($scopes as $scope) { + $period->addScope($scope); + } + + $this->entityManager->persist($period); + self::$periodsIdsToDelete[] = $period->getId(); + + if ($confirm) { + $workflow = $this->registry->get($period, 'accompanying_period_lifecycle'); + $workflow->apply($period, 'confirm'); + } + + return $period; + } +} From a56370d8519af48dc217ee48d789ceab136cacb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 4 Jul 2023 16:00:09 +0200 Subject: [PATCH 10/50] DX: fix phpstan issues with more strict type hinting in AuthorizationHelperInterface --- .../Timeline/TimelineActivityProvider.php | 1 + .../Service/DocGenerator/PersonContext.php | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Bundle/ChillActivityBundle/Timeline/TimelineActivityProvider.php b/src/Bundle/ChillActivityBundle/Timeline/TimelineActivityProvider.php index dad597676..2c9272b08 100644 --- a/src/Bundle/ChillActivityBundle/Timeline/TimelineActivityProvider.php +++ b/src/Bundle/ChillActivityBundle/Timeline/TimelineActivityProvider.php @@ -161,6 +161,7 @@ class TimelineActivityProvider implements TimelineProviderInterface // loop on reachable scopes foreach ($reachableScopes as $scope) { + /** @phpstan-ignore-next-line */ if (in_array($scope->getId(), $scopes_ids, true)) { continue; } diff --git a/src/Bundle/ChillPersonBundle/Service/DocGenerator/PersonContext.php b/src/Bundle/ChillPersonBundle/Service/DocGenerator/PersonContext.php index 9d7f1cdd5..5b5773922 100644 --- a/src/Bundle/ChillPersonBundle/Service/DocGenerator/PersonContext.php +++ b/src/Bundle/ChillPersonBundle/Service/DocGenerator/PersonContext.php @@ -360,10 +360,12 @@ final class PersonContext implements PersonContextInterface private function isScopeNecessary(Person $person): bool { - if ($this->showScopes && 1 < $this->authorizationHelper->getReachableScopes( - $this->security->getUser(), - PersonDocumentVoter::CREATE, - $this->centerResolverManager->resolveCenters($person) + if ($this->showScopes && 1 < count( + $this->authorizationHelper->getReachableScopes( + $this->security->getUser(), + PersonDocumentVoter::CREATE, + $this->centerResolverManager->resolveCenters($person) + ) )) { return true; } From 145c1df313e0328830fe406c12b1afcf03b070dd Mon Sep 17 00:00:00 2001 From: Mathieu Jaumotte Date: Wed, 5 Jul 2023 09:43:13 +0200 Subject: [PATCH 11/50] cleaning --- .../Resources/public/chill/scss/forms.scss | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/forms.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/forms.scss index cd81f36dc..a517a5516 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/forms.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/forms.scss @@ -44,13 +44,5 @@ form { } .chill_filter_order { - background: $gray-100; /* - border: 3px dashed $white; - background: repeating-linear-gradient( - -45deg, - $gray-100, - $gray-100 2px, - $white 2px, - $white 6px - ); */ + background: $gray-100; } \ No newline at end of file From a990591e0cadcb5d2f70b61ab9c60df5c8a88036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 5 Jul 2023 16:23:14 +0200 Subject: [PATCH 12/50] handle right to see confidential course on regulation list --- ...mpanyingPeriodRegulationListController.php | 1 + .../AccompanyingPeriodACLAwareRepository.php | 122 ++++++++++----- ...nyingPeriodACLAwareRepositoryInterface.php | 2 +- ...companyingPeriodACLAwareRepositoryTest.php | 141 +++++++++++++++++- 4 files changed, 229 insertions(+), 37 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingPeriodRegulationListController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingPeriodRegulationListController.php index 6bbb6c368..cd550ef59 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingPeriodRegulationListController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingPeriodRegulationListController.php @@ -78,6 +78,7 @@ class AccompanyingPeriodRegulationListController $form['jobs']->getData(), $form['services']->getData(), $form['locations']->getData(), + ['openingDate' => 'DESC', 'id' => 'DESC'], $paginator->getItemsPerPage(), $paginator->getCurrentPageFirstItemNumber() ); diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php index 104f49ec7..e62e2b3ac 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\PersonBundle\Repository; use Chill\MainBundle\Entity\Address; +use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User; @@ -26,6 +27,8 @@ use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; use DateTime; use DateTimeImmutable; use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use Repository\AccompanyingPeriodACLAwareRepositoryTest; @@ -107,9 +110,16 @@ final readonly class AccompanyingPeriodACLAwareRepository implements Accompanyin return $qb; } + /** + * @throws NonUniqueResultException + * @throws NoResultException + */ public function countByUnDispatched(array $jobs, array $services, array $administrativeLocations): int { - $qb = $this->addACLByUnDispatched($this->buildQueryUnDispatched($jobs, $services, $administrativeLocations)); + $qb = $this->addACLByUnDispatched( + $this->buildQueryUnDispatched($jobs, $services, $administrativeLocations), + $this->buildCenterOnScope() + ); $qb->select('COUNT(ap)'); @@ -194,6 +204,8 @@ final readonly class AccompanyingPeriodACLAwareRepository implements Accompanyin } /** + * Add clause for scope on a query, based on no + * * @param QueryBuilder $qb where the accompanying period have the `ap` alias * @param array $scopesCanSee * @param array $scopesCanSeeConfidential @@ -252,19 +264,27 @@ final readonly class AccompanyingPeriodACLAwareRepository implements Accompanyin return $qb; } - public function findByUnDispatched(array $jobs, array $services, array $administrativeLocations, ?int $limit = null, ?int $offset = null): array + public function buildCenterOnScope(): array { - $qb = $this->addACLByUnDispatched($this->buildQueryUnDispatched($jobs, $services, $administrativeLocations)); + $centerOnScopes = []; + foreach ($this->authorizationHelper->getReachableCenters(AccompanyingPeriodVoter::SEE) as $center) { + $centerOnScopes[] = [ + 'center' => $center, + 'scopeOnRole' => $this->authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE, $center), + 'scopeCanSeeConfidential' => $this->authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, $center), + ]; + } + return $centerOnScopes; + } + + public function findByUnDispatched(array $jobs, array $services, array $administrativeAdministrativeLocations, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array + { + $qb = $this->buildQueryUnDispatched($jobs, $services, $administrativeAdministrativeLocations); $qb->select('ap'); - if (null !== $limit) { - $qb->setMaxResults($limit); - } - - if (null !== $offset) { - $qb->setFirstResult($offset); - } + $qb = $this->addACLByUnDispatched($qb, $this->buildCenterOnScope(), false); + $qb = $this->addOrderLimitClauses($qb, $orderBy, $limit, $offset); return $qb->getQuery()->getResult(); } @@ -305,41 +325,73 @@ final readonly class AccompanyingPeriodACLAwareRepository implements Accompanyin return $qb->getQuery()->getResult(); } - private function addACLByUnDispatched(QueryBuilder $qb): QueryBuilder + /** + * @param QueryBuilder $qb + * @param list, scopeCanSeeConfidential: list}> $centerScopes + * @param bool $allowNoCenter if true, will allow to see the periods linked to person which does not have any center. Very few edge case when some Person are not associated to a center. + * @return QueryBuilder + */ + public function addACLByUnDispatched(QueryBuilder $qb, array $centerScopes, bool $allowNoCenter = false): QueryBuilder { - $centers = $this->authorizationHelper->getReachableCenters( - AccompanyingPeriodVoter::SEE - ); + $user = $this->security->getUser(); - $orX = $qb->expr()->orX(); - - if (0 === count($centers)) { + if (0 === count($centerScopes) || !$user instanceof User) { return $qb->andWhere("'FALSE' = 'TRUE'"); } - foreach ($centers as $key => $center) { - $scopes = $this->authorizationHelper - ->getReachableScopes( - AccompanyingPeriodVoter::SEE, - $center - ); + $orX = $qb->expr()->orX(); + $idx = 0; + foreach ($centerScopes as ['center' => $center, 'scopeOnRole' => $scopes, 'scopeCanSeeConfidential' => $scopesCanSeeConfidential]) { $and = $qb->expr()->andX( - $qb->expr()->exists('SELECT part FROM ' . AccompanyingPeriodParticipation::class . ' part ' . - "JOIN part.person p WHERE part.accompanyingPeriod = ap.id AND p.center = :center_{$key}") + $qb->expr()->exists( + 'SELECT 1 FROM ' . AccompanyingPeriodParticipation::class . " part_{$idx} " . + "JOIN part_{$idx}.person p{$idx} LEFT JOIN p{$idx}.centerCurrent centerCurrent_{$idx} " . + "WHERE part_{$idx}.accompanyingPeriod = ap.id AND (centerCurrent_{$idx}.center = :center_{$idx}" + . ($allowNoCenter ? " OR centerCurrent_{$idx}.id IS NULL)" : ")") + ) ); - $qb->setParameter('center_' . $key, $center); - $orScope = $qb->expr()->orX(); + $qb->setParameter('center_' . $idx, $center); - foreach ($scopes as $skey => $scope) { - $orScope->add( - $qb->expr()->isMemberOf(':scope_' . $key . '_' . $skey, 'ap.scopes') + $orScopeInsideCenter = $qb->expr()->orX( + // even if the scope is not in one authorized, the user can see the course if it is in DRAFT state + $qb->expr()->eq('ap.step', ':draft') + ); + + $idx++; + foreach ($scopes as $scope) { + // for each scope: + // - either the user is the referrer of the course + // - or the accompanying course is one of the reachable scopes + // - and the parcours is not confidential OR the user is the referrer OR the user can see the confidential course + $orOnScope = $qb->expr()->orX( + $qb->expr()->isMemberOf(':scope_' . $idx, 'ap.scopes'), + $qb->expr()->eq('ap.user', ':user') ); - $qb->setParameter('scope_' . $key . '_' . $skey, $scope); + $qb->setParameter('user', $user); + + if (in_array($scope, $scopesCanSeeConfidential, true)) { + $orScopeInsideCenter->add($orOnScope); + } else { + // we must add a condition: the course is not confidential or the user is the referrer + $andXOnScope = $qb->expr()->andX( + $orOnScope, + $qb->expr()->orX( + 'ap.confidential = FALSE', + $qb->expr()->eq('ap.user', ':user') + ) + ); + $orScopeInsideCenter->add($andXOnScope); + } + $qb->setParameter('scope_' . $idx, $scope); + + $idx++; } - $and->add($orScope); + $and->add($orScopeInsideCenter); $orX->add($and); + + $idx++; } return $qb->andWhere($orX); @@ -350,7 +402,7 @@ final readonly class AccompanyingPeriodACLAwareRepository implements Accompanyin * @param array|Scope[] $services * @param array|Location[] $locations */ - private function buildQueryUnDispatched(array $jobs, array $services, array $locations): QueryBuilder + public function buildQueryUnDispatched(array $jobs, array $services, array $locations): QueryBuilder { $qb = $this->accompanyingPeriodRepository->createQueryBuilder('ap'); @@ -378,8 +430,8 @@ final readonly class AccompanyingPeriodACLAwareRepository implements Accompanyin $or = $qb->expr()->orX(); foreach ($services as $key => $service) { - $or->add($qb->expr()->isMemberOf(':scope_' . $key, 'ap.scopes')); - $qb->setParameter('scope_' . $key, $service); + $or->add($qb->expr()->isMemberOf(':scopef_' . $key, 'ap.scopes')); + $qb->setParameter('scopef_' . $key, $service); } $qb->andWhere($or); } diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepositoryInterface.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepositoryInterface.php index ff3d89783..61b631904 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepositoryInterface.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepositoryInterface.php @@ -50,7 +50,7 @@ interface AccompanyingPeriodACLAwareRepositoryInterface * * @return list */ - public function findByUnDispatched(array $jobs, array $services, array $administrativeLocations, ?int $limit = null, ?int $offset = null): array; + public function findByUnDispatched(array $jobs, array $services, array $administrativeAdministrativeLocations, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array; /** * @param array|PostalCode[] $postalCodes diff --git a/src/Bundle/ChillPersonBundle/Tests/Repository/AccompanyingPeriodACLAwareRepositoryTest.php b/src/Bundle/ChillPersonBundle/Tests/Repository/AccompanyingPeriodACLAwareRepositoryTest.php index 17c12fea5..90e0d2384 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Repository/AccompanyingPeriodACLAwareRepositoryTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Repository/AccompanyingPeriodACLAwareRepositoryTest.php @@ -11,8 +11,10 @@ declare(strict_types=1); namespace Repository; +use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Repository\CenterRepositoryInterface; use Chill\MainBundle\Repository\ScopeRepositoryInterface; use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface; use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; @@ -35,10 +37,13 @@ use Symfony\Component\Workflow\Registry; class AccompanyingPeriodACLAwareRepositoryTest extends KernelTestCase { use ProphecyTrait; + private AccompanyingPeriodRepository $accompanyingPeriodRepository; private CenterResolverManagerInterface $centerResolverManager; + private CenterRepositoryInterface $centerRepository; + private EntityManagerInterface $entityManager; private ScopeRepositoryInterface $scopeRepository; @@ -51,11 +56,11 @@ class AccompanyingPeriodACLAwareRepositoryTest extends KernelTestCase { self::bootKernel(); $this->accompanyingPeriodRepository = self::$container->get(AccompanyingPeriodRepository::class); + $this->centerRepository = self::$container->get(CenterRepositoryInterface::class); $this->centerResolverManager = self::$container->get(CenterResolverManagerInterface::class); $this->entityManager = self::$container->get(EntityManagerInterface::class); $this->scopeRepository = self::$container->get(ScopeRepositoryInterface::class); $this->registry = self::$container->get(Registry::class); - } public static function tearDownAfterClass(): void @@ -78,6 +83,140 @@ class AccompanyingPeriodACLAwareRepositoryTest extends KernelTestCase $em->flush(); } + /** + * @dataProvider provideDataFindByUndispatched + * @param list, scopeCanSeeConfidential: list}> $centerScopes + * @param list $expectedContains + * @param list $expectedNotContains + */ + public function testFindByUndispatched(User $user, array $centerScopes, array $expectedContains, array $expectedNotContains, string $message): void + { + $security = $this->prophesize(Security::class); + $security->getUser()->willReturn($user); + + $authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class); + $centers = []; + + foreach ($centerScopes as ['center' => $center, 'scopeOnRole' => $scopes, 'scopeCanSeeConfidential' => $scopesCanSeeConfidential]) { + $centers[spl_object_hash($center)] = $center; + $authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE, $center) + ->willReturn($scopes); + $authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, $center) + ->willReturn($scopesCanSeeConfidential); + } + $authorizationHelper->getReachableCenters(AccompanyingPeriodVoter::SEE)->willReturn(array_values($centers)); + + $repository = new AccompanyingPeriodACLAwareRepository( + $this->accompanyingPeriodRepository, + $security->reveal(), + $authorizationHelper->reveal(), + $this->centerResolverManager + ); + + $actual = array_map( + fn (AccompanyingPeriod $period) => $period->getId(), + $repository->findByUnDispatched([], [], [], ['id' => 'DESC'], 20, 0) + ); + + foreach ($expectedContains as $expected) { + self::assertContains($expected->getId(), $actual, $message); + } + foreach ($expectedNotContains as $expected) { + self::assertNotContains($expected->getId(), $actual, $message); + } + } + + public function provideDataFindByUndispatched(): iterable + { + $this->setUp(); + + if (null === $user = $this->entityManager->createQuery("SELECT u FROM " . User::class . " u")->setMaxResults(1)->getSingleResult()) { + throw new \RuntimeException("no user found"); + } + + if (null === $anotherUser = $this->entityManager->createQuery("SELECT u FROM " . User::class . " u WHERE u.id != :uid")->setParameter('uid', $user->getId()) + ->setMaxResults(1)->getSingleResult()) { + throw new \RuntimeException("no user found"); + } + + /** @var Person $person */ + [$person, $anotherPerson, $person2, $person3] = $this->entityManager + ->createQuery("SELECT p FROM " . Person::class . " p ") + ->setMaxResults(4) + ->getResult(); + + if (null === $person || null === $anotherPerson || null === $person2 || null === $person3) { + throw new \RuntimeException("no person found"); + } + + $scopes = $this->scopeRepository->findAll(); + + if (3 > count($scopes)) { + throw new \RuntimeException("not enough scopes for this test"); + } + $scopesCanSee = [ $scopes[0] ]; + $scopesGroup2 = [ $scopes[1] ]; + + $centers = $this->centerRepository->findActive(); + + if (2 > count($centers)) { + throw new \RuntimeException("not enough centers for this test"); + } + + $period = $this->buildPeriod($person, $scopesCanSee, $user, true); + + // expected scope: can see the period + yield [ + $anotherUser, + [ + [ + 'center' => $person->getCenter(), + 'scopeOnRole' => $scopesCanSee, + 'scopeCanSeeConfidential' => [], + ], + ], + [$period], + [], + "period should be visible with expected scopes", + ]; + + // no scope visible + yield [ + $anotherUser, + [ + [ + 'center' => $person->getCenter(), + 'scopeOnRole' => $scopesGroup2, + 'scopeCanSeeConfidential' => [], + ], + ], + [], + [$period], + "period should not be visible without expected scopes", + ]; + + // another center + yield [ + $anotherUser, + [ + [ + 'center' => $person->getCenter(), + 'scopeOnRole' => $scopesGroup2, + 'scopeCanSeeConfidential' => [], + ], + [ + 'center' => array_values(array_filter($centers, fn (Center $c) => $c !== $person->getCenter()))[0], + 'scopeOnRole' => $scopesCanSee, + 'scopeCanSeeConfidential' => [], + ], + ], + [], + [$period], + "period should not be visible for user having right in another scope (with multiple centers)" + ]; + + $this->entityManager->flush(); + } /** * For testing this method, we mock the authorization helper to return different Scope that a user From 779eb812b0f542a5c2ccb64dc92fc1aa18c6317c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 5 Jul 2023 21:56:50 +0200 Subject: [PATCH 13/50] Add new role to see confidential right on method AccompanyingPeriodACLAwareRepositoryInterface::findByUserAndPostalCodeOpenedAccompanyingPeriod --- .../ReassignAccompanyingPeriodController.php | 2 +- .../AccompanyingPeriodACLAwareRepository.php | 102 +++------- ...nyingPeriodACLAwareRepositoryInterface.php | 12 +- ...companyingPeriodACLAwareRepositoryTest.php | 174 +++++++++++++++++- 4 files changed, 200 insertions(+), 90 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Controller/ReassignAccompanyingPeriodController.php b/src/Bundle/ChillPersonBundle/Controller/ReassignAccompanyingPeriodController.php index fde9746d3..3e5b59c2a 100644 --- a/src/Bundle/ChillPersonBundle/Controller/ReassignAccompanyingPeriodController.php +++ b/src/Bundle/ChillPersonBundle/Controller/ReassignAccompanyingPeriodController.php @@ -98,7 +98,7 @@ class ReassignAccompanyingPeriodController extends AbstractController $userFrom = $form['user']->getData(); $postalCodes = $form['postal_code']->getData() instanceof PostalCode ? [$form['postal_code']->getData()] : []; - $total = $this->accompanyingPeriodACLAwareRepository->countByUserOpenedAccompanyingPeriod($userFrom); + $total = $this->accompanyingPeriodACLAwareRepository->countByUserAndPostalCodesOpenedAccompanyingPeriod($userFrom, $postalCodes); $paginator = $this->paginatorFactory->create($total); $paginator->setItemsPerPage(50); $periods = $this->accompanyingPeriodACLAwareRepository diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php index e62e2b3ac..79a7710ff 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php @@ -14,6 +14,7 @@ namespace Chill\PersonBundle\Repository; use Chill\MainBundle\Entity\Address; use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\Location; +use Chill\MainBundle\Entity\PostalCode; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\UserJob; @@ -21,12 +22,8 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInt use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; -use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress; use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; -use DateTime; -use DateTimeImmutable; -use Doctrine\DBAL\Types\Types; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; use Doctrine\ORM\Query\Expr\Join; @@ -60,51 +57,33 @@ final readonly class AccompanyingPeriodACLAwareRepository implements Accompanyin $this->centerResolver = $centerResolverDispatcher; } - public function buildQueryOpenedAccompanyingCourseByUser(?User $user, array $postalCodes = []): QueryBuilder + public function buildQueryOpenedAccompanyingCourseByUserAndPostalCodes(?User $user, array $postalCodes = []): QueryBuilder { $qb = $this->accompanyingPeriodRepository->createQueryBuilder('ap'); $qb->where($qb->expr()->eq('ap.user', ':user')) ->andWhere( $qb->expr()->neq('ap.step', ':draft'), - $qb->expr()->orX( - $qb->expr()->isNull('ap.closingDate'), - $qb->expr()->gt('ap.closingDate', ':now') - ) + $qb->expr()->neq('ap.step', ':closed'), ) ->setParameter('user', $user) - ->setParameter('now', new DateTime('now')) - ->setParameter('draft', AccompanyingPeriod::STEP_DRAFT); + ->setParameter('draft', AccompanyingPeriod::STEP_DRAFT) + ->setParameter('closed', AccompanyingPeriod::STEP_CLOSED); if ([] !== $postalCodes) { - $qb->join('ap.locationHistories', 'location_history') - ->leftJoin(PersonHouseholdAddress::class, 'person_address', Join::WITH, 'IDENTITY(location_history.personLocation) = IDENTITY(person_address.person)') + $qb->join('ap.locationHistories', 'location_history', Join::WITH, 'location_history.endDate IS NULL') + ->leftJoin(Person\PersonCurrentAddress::class, 'person_address', Join::WITH, 'IDENTITY(location_history.personLocation) = IDENTITY(person_address.person)') ->join( Address::class, 'address', Join::WITH, - 'COALESCE(IDENTITY(location_history.addressLocation), IDENTITY(person_address.address)) = address.id' + 'COALESCE(IDENTITY(person_address.address), IDENTITY(location_history.addressLocation)) = address.id' ) + ->join('address.postcode', 'postcode') ->andWhere( - $qb->expr()->orX( - $qb->expr()->isNull('person_address'), - $qb->expr()->andX( - $qb->expr()->lte('person_address.validFrom', ':now'), - $qb->expr()->orX( - $qb->expr()->isNull('person_address.validTo'), - $qb->expr()->lt('person_address.validTo', ':now') - ) - ) - ) + $qb->expr()->in('postcode.code', ':postal_codes') ) - ->andWhere( - $qb->expr()->isNull('location_history.endDate') - ) - ->andWhere( - $qb->expr()->in('address.postcode', ':postal_codes') - ) - ->setParameter('now', new DateTimeImmutable('now'), Types::DATE_IMMUTABLE) - ->setParameter('postal_codes', $postalCodes); + ->setParameter('postal_codes', array_map(fn (PostalCode $postalCode) => $postalCode->getCode(), $postalCodes)); } return $qb; @@ -116,7 +95,7 @@ final readonly class AccompanyingPeriodACLAwareRepository implements Accompanyin */ public function countByUnDispatched(array $jobs, array $services, array $administrativeLocations): int { - $qb = $this->addACLByUnDispatched( + $qb = $this->addACLMultiCenterOnQuery( $this->buildQueryUnDispatched($jobs, $services, $administrativeLocations), $this->buildCenterOnScope() ); @@ -132,22 +111,12 @@ final readonly class AccompanyingPeriodACLAwareRepository implements Accompanyin return 0; } - return $this->buildQueryOpenedAccompanyingCourseByUser($user, $postalCodes) - ->select('COUNT(ap)') - ->getQuery() - ->getSingleScalarResult(); - } + $qb = $this->buildQueryOpenedAccompanyingCourseByUserAndPostalCodes($user, $postalCodes); + $qb = $this->addACLMultiCenterOnQuery($qb, $this->buildCenterOnScope(), false); - public function countByUserOpenedAccompanyingPeriod(?User $user): int - { - if (null === $user) { - return 0; - } + $qb->select('COUNT(DISTINCT ap)'); - return $this->buildQueryOpenedAccompanyingCourseByUser($user) - ->select('COUNT(ap)') - ->getQuery() - ->getSingleScalarResult(); + return $qb->getQuery()->getSingleScalarResult(); } public function findByPerson( @@ -283,7 +252,7 @@ final readonly class AccompanyingPeriodACLAwareRepository implements Accompanyin $qb = $this->buildQueryUnDispatched($jobs, $services, $administrativeAdministrativeLocations); $qb->select('ap'); - $qb = $this->addACLByUnDispatched($qb, $this->buildCenterOnScope(), false); + $qb = $this->addACLMultiCenterOnQuery($qb, $this->buildCenterOnScope(), false); $qb = $this->addOrderLimitClauses($qb, $orderBy, $limit, $offset); return $qb->getQuery()->getResult(); @@ -295,32 +264,9 @@ final readonly class AccompanyingPeriodACLAwareRepository implements Accompanyin return []; } - $qb = $this->buildQueryOpenedAccompanyingCourseByUser($user); - - $qb->setFirstResult($offset) - ->setMaxResults($limit); - - foreach ($orderBy as $field => $direction) { - $qb->addOrderBy('ap.' . $field, $direction); - } - - return $qb->getQuery()->getResult(); - } - - public function findByUserOpenedAccompanyingPeriod(?User $user, array $orderBy = [], int $limit = 0, int $offset = 50): array - { - if (null === $user) { - return []; - } - - $qb = $this->buildQueryOpenedAccompanyingCourseByUser($user); - - $qb->setFirstResult($offset) - ->setMaxResults($limit); - - foreach ($orderBy as $field => $direction) { - $qb->addOrderBy('ap.' . $field, $direction); - } + $qb = $this->buildQueryOpenedAccompanyingCourseByUserAndPostalCodes($user, $postalCodes); + $qb = $this->addACLMultiCenterOnQuery($qb, $this->buildCenterOnScope(), false); + $qb = $this->addOrderLimitClauses($qb, $orderBy, $limit, $offset); return $qb->getQuery()->getResult(); } @@ -331,7 +277,7 @@ final readonly class AccompanyingPeriodACLAwareRepository implements Accompanyin * @param bool $allowNoCenter if true, will allow to see the periods linked to person which does not have any center. Very few edge case when some Person are not associated to a center. * @return QueryBuilder */ - public function addACLByUnDispatched(QueryBuilder $qb, array $centerScopes, bool $allowNoCenter = false): QueryBuilder + public function addACLMultiCenterOnQuery(QueryBuilder $qb, array $centerScopes, bool $allowNoCenter = false): QueryBuilder { $user = $this->security->getUser(); @@ -366,9 +312,9 @@ final readonly class AccompanyingPeriodACLAwareRepository implements Accompanyin // - and the parcours is not confidential OR the user is the referrer OR the user can see the confidential course $orOnScope = $qb->expr()->orX( $qb->expr()->isMemberOf(':scope_' . $idx, 'ap.scopes'), - $qb->expr()->eq('ap.user', ':user') + $qb->expr()->eq('ap.user', ':user_executing') ); - $qb->setParameter('user', $user); + $qb->setParameter('user_executing', $user); if (in_array($scope, $scopesCanSeeConfidential, true)) { $orScopeInsideCenter->add($orOnScope); @@ -378,7 +324,7 @@ final readonly class AccompanyingPeriodACLAwareRepository implements Accompanyin $orOnScope, $qb->expr()->orX( 'ap.confidential = FALSE', - $qb->expr()->eq('ap.user', ':user') + $qb->expr()->eq('ap.user', ':user_executing') ) ); $orScopeInsideCenter->add($andXOnScope); diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepositoryInterface.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepositoryInterface.php index 61b631904..7b31887b9 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepositoryInterface.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepositoryInterface.php @@ -31,8 +31,6 @@ interface AccompanyingPeriodACLAwareRepositoryInterface */ public function countByUserAndPostalCodesOpenedAccompanyingPeriod(?User $user, array $postalCodes): int; - public function countByUserOpenedAccompanyingPeriod(?User $user): int; - /** * @return array */ @@ -40,8 +38,8 @@ interface AccompanyingPeriodACLAwareRepositoryInterface Person $person, string $role, ?array $orderBy = [], - ?int $limit = null, - ?int $offset = null + ?int $limit = null, + ?int $offset = null ): array; /** @@ -57,10 +55,4 @@ interface AccompanyingPeriodACLAwareRepositoryInterface * @return list */ public function findByUserAndPostalCodesOpenedAccompanyingPeriod(?User $user, array $postalCodes, array $orderBy = [], int $limit = 0, int $offset = 50): array; - - /** - * @deprecated - * @return list - */ - public function findByUserOpenedAccompanyingPeriod(?User $user, array $orderBy = [], int $limit = 0, int $offset = 50): array; } diff --git a/src/Bundle/ChillPersonBundle/Tests/Repository/AccompanyingPeriodACLAwareRepositoryTest.php b/src/Bundle/ChillPersonBundle/Tests/Repository/AccompanyingPeriodACLAwareRepositoryTest.php index 90e0d2384..cb8d56ba3 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Repository/AccompanyingPeriodACLAwareRepositoryTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Repository/AccompanyingPeriodACLAwareRepositoryTest.php @@ -80,7 +80,178 @@ class AccompanyingPeriodACLAwareRepositoryTest extends KernelTestCase $em->remove($period); } - $em->flush(); + //$em->flush(); + } + + /** + * @dataProvider provideDataFindByUserAndPostalCodesOpenedAccompanyingPeriod + * @param list, scopeCanSeeConfidential: list}> $centerScopes + * @param list $expectedContains + * @param list $expectedNotContains + */ + public function testFindByUserAndPostalCodesOpenedAccompanyingPeriod(User $user, User $searched, array $centerScopes, array $expectedContains, array $expectedNotContains, string $message): void + { + $security = $this->prophesize(Security::class); + $security->getUser()->willReturn($user); + + $authorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class); + $centers = []; + + foreach ($centerScopes as ['center' => $center, 'scopeOnRole' => $scopes, 'scopeCanSeeConfidential' => $scopesCanSeeConfidential]) { + $centers[spl_object_hash($center)] = $center; + $authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE, $center) + ->willReturn($scopes); + $authorizationHelper->getReachableScopes(AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, $center) + ->willReturn($scopesCanSeeConfidential); + } + $authorizationHelper->getReachableCenters(AccompanyingPeriodVoter::SEE)->willReturn(array_values($centers)); + + $repository = new AccompanyingPeriodACLAwareRepository( + $this->accompanyingPeriodRepository, + $security->reveal(), + $authorizationHelper->reveal(), + $this->centerResolverManager + ); + + $actual = array_map( + fn (AccompanyingPeriod $period) => $period->getId(), + $repository->findByUserAndPostalCodesOpenedAccompanyingPeriod($searched, [], ['id' => 'DESC'], 20, 0) + ); + + foreach ($expectedContains as $expected) { + self::assertContains($expected->getId(), $actual, $message); + } + foreach ($expectedNotContains as $expected) { + self::assertNotContains($expected->getId(), $actual, $message); + } + } + + public function provideDataFindByUserAndPostalCodesOpenedAccompanyingPeriod(): iterable + { + $this->setUp(); + + if (null === $user = $this->entityManager->createQuery("SELECT u FROM " . User::class . " u")->setMaxResults(1)->getSingleResult()) { + throw new \RuntimeException("no user found"); + } + + if (null === $anotherUser = $this->entityManager->createQuery("SELECT u FROM " . User::class . " u WHERE u.id != :uid")->setParameter('uid', $user->getId()) + ->setMaxResults(1)->getSingleResult()) { + throw new \RuntimeException("no user found"); + } + + /** @var Person $person */ + [$person, $anotherPerson, $person2, $person3] = $this->entityManager + ->createQuery("SELECT p FROM " . Person::class . " p JOIN p.centerCurrent current_center") + ->setMaxResults(4) + ->getResult(); + + if (null === $person || null === $anotherPerson || null === $person2 || null === $person3) { + throw new \RuntimeException("no person found"); + } + + $scopes = $this->scopeRepository->findAll(); + + if (3 > count($scopes)) { + throw new \RuntimeException("not enough scopes for this test"); + } + $scopesCanSee = [ $scopes[0] ]; + $scopesGroup2 = [ $scopes[1] ]; + + $centers = $this->centerRepository->findActive(); + $aCenterNotAssociatedToPerson = array_values(array_filter($centers, fn (Center $c) => $c !== $person->getCenter()))[0]; + + if (2 > count($centers)) { + throw new \RuntimeException("not enough centers for this test"); + } + + $period = $this->buildPeriod($person, $scopesCanSee, $user, true); + $period->setUser($user); + + yield [ + $anotherUser, + $user, + [ + [ + 'center' => $person->getCenter(), + 'scopeOnRole' => $scopesCanSee, + 'scopeCanSeeConfidential' => [], + ], + ], + [$period], + [], + "period should be visible with expected scopes", + ]; + + yield [ + $anotherUser, + $user, + [ + [ + 'center' => $person->getCenter(), + 'scopeOnRole' => $scopesGroup2, + 'scopeCanSeeConfidential' => [], + ], + ], + [], + [$period], + "period should not be visible without expected scopes", + ]; + + yield [ + $anotherUser, + $user, + [ + [ + 'center' => $person->getCenter(), + 'scopeOnRole' => $scopesGroup2, + 'scopeCanSeeConfidential' => [], + ], + [ + 'center' => $aCenterNotAssociatedToPerson, + 'scopeOnRole' => $scopesCanSee, + 'scopeCanSeeConfidential' => [], + ], + ], + [], + [$period], + "period should not be visible for user having right in another scope (with multiple centers)" + ]; + + $period = $this->buildPeriod($person, $scopesCanSee, $user, true); + $period->setUser($user); + $period->setConfidential(true); + + yield [ + $anotherUser, + $user, + [ + [ + 'center' => $person->getCenter(), + 'scopeOnRole' => $scopesCanSee, + 'scopeCanSeeConfidential' => [], + ], + ], + [], + [$period], + "period confidential should not be visible", + ]; + + yield [ + $anotherUser, + $user, + [ + [ + 'center' => $person->getCenter(), + 'scopeOnRole' => $scopesCanSee, + 'scopeCanSeeConfidential' => $scopesCanSee, + ], + ], + [$period], + [], + "period confidential be visible if user has required scopes", + ]; + + $this->entityManager->flush(); } /** @@ -165,6 +336,7 @@ class AccompanyingPeriodACLAwareRepositoryTest extends KernelTestCase $period = $this->buildPeriod($person, $scopesCanSee, $user, true); + // expected scope: can see the period yield [ $anotherUser, From ff1629cbb7d6719c381d86cea6bb54d658534202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 5 Jul 2023 22:06:21 +0200 Subject: [PATCH 14/50] Separate role "see confidential course" from "reassign bulk" --- .../DependencyInjection/ChillPersonExtension.php | 4 ---- .../Security/Authorization/AccompanyingPeriodVoter.php | 1 + src/Bundle/ChillPersonBundle/translations/messages.fr.yml | 3 ++- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php index 6fd434635..121bbba14 100644 --- a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php +++ b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php @@ -983,10 +983,6 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac AccompanyingPeriodVoter::EDIT, AccompanyingPeriodVoter::DELETE, ], - AccompanyingPeriodVoter::REASSIGN_BULK => [ - AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, - AccompanyingPeriodVoter::TOGGLE_CONFIDENTIAL_ALL, - ], AccompanyingPeriodVoter::TOGGLE_CONFIDENTIAL_ALL => [ AccompanyingPeriodVoter::SEE_CONFIDENTIAL_ALL, ], diff --git a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php index 3dd991501..795a921e7 100644 --- a/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php +++ b/src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodVoter.php @@ -138,6 +138,7 @@ class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleH self::TOGGLE_CONFIDENTIAL_ALL, self::REASSIGN_BULK, self::STATS, + self::SEE_CONFIDENTIAL_ALL, ]; } diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 01383c050..635f86c99 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -329,8 +329,9 @@ CHILL_PERSON_ACCOMPANYING_PERIOD_CREATE: Créer un parcours d'accompagnement CHILL_PERSON_ACCOMPANYING_PERIOD_UPDATE: Modifier un parcours d'accompagnement CHILL_PERSON_ACCOMPANYING_PERIOD_FULL: Voir les détails, créer, supprimer et mettre à jour un parcours d'accompagnement CHILL_PERSON_ACCOMPANYING_COURSE_REASSIGN_BULK: Réassigner les parcours en lot -CHILL_PERSON_ACCOMPANYING_PERIOD_SEE_DETAILS: Voir les détails d'un parcours d'accompagnement +CHILL_PERSON_ACCOMPANYING_PERIOD_SEE_DETAILS: Voir les détails d'un parcours d'accompagnement CHILL_PERSON_ACCOMPANYING_PERIOD_STATS: Statistiques sur les parcours d'accompagnement +CHILL_PERSON_ACCOMPANYING_PERIOD_SEE_CONFIDENTIAL: Voir les parcours confidentiels CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_CREATE: Créer une action d'accompagnement CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_DELETE: Supprimer une action d'accompagnement From af4e7f1226bad8fb4e5327ddf515f7dc11a0369f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 5 Jul 2023 22:06:36 +0200 Subject: [PATCH 15/50] Add changie entry --- .changes/unreleased/Feature-20230705-220544.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changes/unreleased/Feature-20230705-220544.yaml diff --git a/.changes/unreleased/Feature-20230705-220544.yaml b/.changes/unreleased/Feature-20230705-220544.yaml new file mode 100644 index 000000000..4212f7646 --- /dev/null +++ b/.changes/unreleased/Feature-20230705-220544.yaml @@ -0,0 +1,6 @@ +kind: Feature +body: Create a role "See Confidential Periods", separated from the "Reassign courses" + role +time: 2023-07-05T22:05:44.435112463+02:00 +custom: + Issue: "121" From c04fd66163f2014ac54362f56bafebca9f38e1ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 5 Jul 2023 22:20:27 +0200 Subject: [PATCH 16/50] do not show filter on job or activity type if less than 2 possibilities --- .../Controller/ActivityController.php | 39 +++++++++++-------- .../Templating/Listing/FilterOrderHelper.php | 20 ++++++++++ 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php index 63639c149..9b6e69bf0 100644 --- a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php +++ b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php @@ -259,8 +259,8 @@ final class ActivityController extends AbstractController $filterArgs = [ 'my_activities' => $filter->getSingleCheckboxData('my_activities'), - 'types' => $filter->getEntityChoiceData('activity_types'), - 'jobs' => $filter->getEntityChoiceData('jobs'), + 'types' => $filter->hasEntityChoice('activity_type') ? $filter->getEntityChoiceData('activity_types') : [], + 'jobs' => $filter->hasEntityChoice('jobs') ? $filter->getEntityChoiceData('jobs') : [], 'before' => $filter->getDateRangeData('activity_date')['to'], 'after' => $filter->getDateRangeData('activity_date')['from'], ]; @@ -327,21 +327,28 @@ final class ActivityController extends AbstractController $filterBuilder ->addDateRange('activity_date', 'activity.date') - ->addSingleCheckbox('my_activities', 'activity_filter.My activities') - ->addEntityChoice('activity_types', 'activity_filter.Types', \Chill\ActivityBundle\Entity\ActivityType::class, $types, [ - 'choice_label' => function (\Chill\ActivityBundle\Entity\ActivityType $activityType) { - $text = match ($activityType->hasCategory()) { - true => $this->translatableStringHelper->localize($activityType->getCategory()->getName()) . ' > ', - false => '', - }; + ->addSingleCheckbox('my_activities', 'activity_filter.My activities'); - return $text . $this->translatableStringHelper->localize($activityType->getName()); - } - ]) - ->addEntityChoice('jobs', 'activity_filter.Jobs', UserJob::class, $jobs, [ - 'choice_label' => fn (UserJob $u) => $this->translatableStringHelper->localize($u->getLabel()) - ]) - ; + if (1 < count($types)) { + $filterBuilder + ->addEntityChoice('activity_types', 'activity_filter.Types', \Chill\ActivityBundle\Entity\ActivityType::class, $types, [ + 'choice_label' => function (\Chill\ActivityBundle\Entity\ActivityType $activityType) { + $text = match ($activityType->hasCategory()) { + true => $this->translatableStringHelper->localize($activityType->getCategory()->getName()) . ' > ', + false => '', + }; + + return $text . $this->translatableStringHelper->localize($activityType->getName()); + } + ]); + } + + if (1 < count($jobs)) { + $filterBuilder + ->addEntityChoice('jobs', 'activity_filter.Jobs', UserJob::class, $jobs, [ + 'choice_label' => fn (UserJob $u) => $this->translatableStringHelper->localize($u->getLabel()) + ]); + } return $filterBuilder->build(); } diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php index 28cc8e331..5c9971890 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php @@ -116,16 +116,31 @@ class FilterOrderHelper ->handleRequest($this->requestStack->getCurrentRequest()); } + public function hasCheckboxData(string $name): bool + { + return array_key_exists($name, $this->checkboxes); + } + public function getCheckboxData(string $name): array { return $this->getFormData()['checkboxes'][$name]; } + public function hasSingleCheckboxData(string $name): bool + { + return array_key_exists($name, $this->singleCheckbox); + } + public function getSingleCheckboxData(string $name): ?bool { return $this->getFormData()['single_checkboxes'][$name]; } + public function hasEntityChoice(string $name): bool + { + return array_key_exists($name, $this->entityChoices); + } + public function getEntityChoiceData($name): mixed { return $this->getFormData()['entity_choices'][$name]; @@ -144,6 +159,11 @@ class FilterOrderHelper return $this->singleCheckbox; } + public function hasDateRangeData(string $name): bool + { + return array_key_exists($name, $this->dateRanges); + } + /** * @return array{to: ?DateTimeImmutable, from: ?DateTimeImmutable} */ From c19232de35e72cadf0bc5978af2e8d6d79b50e8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 5 Jul 2023 22:37:51 +0200 Subject: [PATCH 17/50] DX: fix phpstan issues --- .../ChillEventBundle/Controller/ParticipationController.php | 5 ++++- src/Bundle/ChillEventBundle/Entity/Event.php | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Bundle/ChillEventBundle/Controller/ParticipationController.php b/src/Bundle/ChillEventBundle/Controller/ParticipationController.php index 1929beac1..a2a82c88f 100644 --- a/src/Bundle/ChillEventBundle/Controller/ParticipationController.php +++ b/src/Bundle/ChillEventBundle/Controller/ParticipationController.php @@ -16,6 +16,8 @@ use Chill\EventBundle\Entity\Event; use Chill\EventBundle\Entity\Participation; use Chill\EventBundle\Form\ParticipationType; use Chill\EventBundle\Security\Authorization\ParticipationVoter; +use Doctrine\Common\Collections\Collection; +use Doctrine\Common\Collections\ReadableCollection; use LogicException; use Psr\Log\LoggerInterface; use RuntimeException; @@ -509,7 +511,7 @@ class ParticipationController extends AbstractController /** * @return \Symfony\Component\Form\FormInterface */ - protected function createEditFormMultiple(ArrayIterator $participations, Event $event) + protected function createEditFormMultiple(Collection $participations, Event $event) { $form = $this->createForm( \Symfony\Component\Form\Extension\Core\Type\FormType::class, @@ -638,6 +640,7 @@ class ParticipationController extends AbstractController $ignoredParticipations = $newParticipations = []; foreach ($participations as $participation) { + /** @var Participation $participation */ // check for authorization $this->denyAccessUnlessGranted( ParticipationVoter::CREATE, diff --git a/src/Bundle/ChillEventBundle/Entity/Event.php b/src/Bundle/ChillEventBundle/Entity/Event.php index d55a7b8a8..cc762e979 100644 --- a/src/Bundle/ChillEventBundle/Entity/Event.php +++ b/src/Bundle/ChillEventBundle/Entity/Event.php @@ -160,11 +160,11 @@ class Event implements HasCenterInterface, HasScopeInterface } /** - * @return ArrayIterator|Collection|Traversable + * @return Collection */ public function getParticipations() { - return $this->getParticipationsOrdered(); + return new ArrayCollection(iterator_to_array($this->getParticipationsOrdered())); } /** From 5b42b85b503c23eda24d5c774d05f4524fc70a72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 6 Jul 2023 16:58:24 +0200 Subject: [PATCH 18/50] Read absence from MS graph api --- .../Exception/UserAbsenceSyncException.php | 20 ++ .../Connector/MSGraph/MSUserAbsenceReader.php | 68 +++++++ .../MSGraph/MSUserAbsenceReaderTest.php | 176 ++++++++++++++++++ 3 files changed, 264 insertions(+) create mode 100644 src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php create mode 100644 src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php create mode 100644 src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderTest.php diff --git a/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php b/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php new file mode 100644 index 000000000..b46c93976 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php @@ -0,0 +1,20 @@ +mapCalendarToUser->getUserId($user); + + if (null === $id) { + return null; + } + + try { + $automaticRepliesSettings = $this->machineHttpClient + ->request('GET', '/users/' . $id . '/mailboxSettings/automaticRepliesSetting') + ->toArray(true); + } catch (ClientExceptionInterface|DecodingExceptionInterface|RedirectionExceptionInterface|TransportExceptionInterface $e) { + throw new UserAbsenceSyncException("Error receiving response for mailboxSettings", 0, $e); + } catch (ServerExceptionInterface $e) { + throw new UserAbsenceSyncException("Server error receiving response for mailboxSettings", 0, $e); + } + + if (!array_key_exists("status", $automaticRepliesSettings)) { + throw new \LogicException("no key \"status\" on automatic replies settings: " . json_encode($automaticRepliesSettings)); + } + + return match ($automaticRepliesSettings['status']) { + 'disabled' => false, + 'alwaysEnabled' => true, + 'scheduled' => + RemoteEventConverter::convertStringDateWithoutTimezone($automaticRepliesSettings['scheduledStartDateTime']['dateTime']) < $this->clock->now() + && RemoteEventConverter::convertStringDateWithoutTimezone($automaticRepliesSettings['scheduledEndDateTime']['dateTime']) > $this->clock->now(), + default => throw new UserAbsenceSyncException("this status is not documented by Microsoft") + }; + } + +} diff --git a/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderTest.php b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderTest.php new file mode 100644 index 000000000..089477fda --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderTest.php @@ -0,0 +1,176 @@ +prophesize(MapCalendarToUser::class); + $mapUser->getUserId($user)->willReturn('1234'); + $clock = new MockClock(new \DateTimeImmutable('2023-07-07T12:00:00')); + + $absenceReader = new MSUserAbsenceReader($client, $mapUser->reveal(), $clock); + + self::assertEquals($expected, $absenceReader->isUserAbsent($user), $message); + } + + public function testIsUserAbsentWithoutRemoteId(): void + { + $user = new User(); + $client = new MockHttpClient(); + + $mapUser = $this->prophesize(MapCalendarToUser::class); + $mapUser->getUserId($user)->willReturn(null); + $clock = new MockClock(new \DateTimeImmutable('2023-07-07T12:00:00')); + + $absenceReader = new MSUserAbsenceReader($client, $mapUser->reveal(), $clock); + + self::assertNull($absenceReader->isUserAbsent($user), "when no user found, absence should be null"); + } + + public function provideDataTestUserAbsence(): iterable + { + // contains data that was retrieved from microsoft graph api on 2023-07-06 + + yield [ + <<<'JSON' + { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/mailboxSettings/automaticRepliesSetting", + "status": "disabled", + "externalAudience": "none", + "internalReplyMessage": "Je suis en congé.", + "externalReplyMessage": "", + "scheduledStartDateTime": { + "dateTime": "2023-07-06T12:00:00.0000000", + "timeZone": "UTC" + }, + "scheduledEndDateTime": { + "dateTime": "2023-07-07T12:00:00.0000000", + "timeZone": "UTC" + } + } + JSON, + false, + "User is present" + ]; + + yield [ + <<<'JSON' + { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/mailboxSettings/automaticRepliesSetting", + "status": "scheduled", + "externalAudience": "none", + "internalReplyMessage": "Je suis en congé.", + "externalReplyMessage": "", + "scheduledStartDateTime": { + "dateTime": "2023-07-06T11:00:00.0000000", + "timeZone": "UTC" + }, + "scheduledEndDateTime": { + "dateTime": "2023-07-21T11:00:00.0000000", + "timeZone": "UTC" + } + } + JSON, + true, + 'User is absent with absence scheduled, we are within this period' + ]; + + yield [ + <<<'JSON' + { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/mailboxSettings/automaticRepliesSetting", + "status": "scheduled", + "externalAudience": "none", + "internalReplyMessage": "Je suis en congé.", + "externalReplyMessage": "", + "scheduledStartDateTime": { + "dateTime": "2023-07-08T11:00:00.0000000", + "timeZone": "UTC" + }, + "scheduledEndDateTime": { + "dateTime": "2023-07-21T11:00:00.0000000", + "timeZone": "UTC" + } + } + JSON, + false, + 'User is present: absence is scheduled for later' + ]; + + yield [ + <<<'JSON' + { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/mailboxSettings/automaticRepliesSetting", + "status": "scheduled", + "externalAudience": "none", + "internalReplyMessage": "Je suis en congé.", + "externalReplyMessage": "", + "scheduledStartDateTime": { + "dateTime": "2023-07-05T11:00:00.0000000", + "timeZone": "UTC" + }, + "scheduledEndDateTime": { + "dateTime": "2023-07-06T11:00:00.0000000", + "timeZone": "UTC" + } + } + JSON, + false, + 'User is present: absence is past' + ]; + + yield [ + <<<'JSON' + { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/mailboxSettings/automaticRepliesSetting", + "status": "alwaysEnabled", + "externalAudience": "none", + "internalReplyMessage": "Je suis en congé.", + "externalReplyMessage": "", + "scheduledStartDateTime": { + "dateTime": "2023-07-06T12:00:00.0000000", + "timeZone": "UTC" + }, + "scheduledEndDateTime": { + "dateTime": "2023-07-07T12:00:00.0000000", + "timeZone": "UTC" + } + } + JSON, + true, + "User is absent: absence is always enabled" + ]; + } + +} From 2861945a5251ed77d50bfe5a8e1fc6083234ba6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 6 Jul 2023 17:29:10 +0200 Subject: [PATCH 19/50] Syncer for user absence, from the msgraph reader --- .../Exception/UserAbsenceSyncException.php | 4 +- .../Connector/MSGraph/MSUserAbsenceReader.php | 2 +- .../MSGraph/MSUserAbsenceReaderInterface.php | 22 ++++++ .../Connector/MSGraph/MSUserAbsenceSync.php | 44 ++++++++++++ .../MSGraph/MSUserAbsenceSyncTest.php | 67 +++++++++++++++++++ 5 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderInterface.php create mode 100644 src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSync.php create mode 100644 src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSyncTest.php diff --git a/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php b/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php index b46c93976..558774fc5 100644 --- a/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php +++ b/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php @@ -13,8 +13,8 @@ namespace Chill\CalendarBundle\Exception; class UserAbsenceSyncException extends \LogicException { - public function __construct(string $message = "", int $code = 20230706) + public function __construct(string $message = "", int $code = 20230706, ?\Throwable $previous = null) { - parent::__construct($message, $code); + parent::__construct($message, $code, $previous); } } diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php index b79a55052..83376528f 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php @@ -21,7 +21,7 @@ use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; -readonly class MSUserAbsenceReader +final readonly class MSUserAbsenceReader implements MSUserAbsenceReaderInterface { public function __construct( private HttpClientInterface $machineHttpClient, diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderInterface.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderInterface.php new file mode 100644 index 000000000..a918bb7ea --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderInterface.php @@ -0,0 +1,22 @@ +absenceReader->isUserAbsent($user); + + if (null === $absence) { + return; + } + + if ($absence === $user->isAbsent()) { + // nothing to do + return; + } + + if ($absence) { + $user->setAbsenceStart($this->clock->now()); + } else { + $user->setAbsenceStart(null); + } + } +} diff --git a/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSyncTest.php b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSyncTest.php new file mode 100644 index 000000000..ddd92086d --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSyncTest.php @@ -0,0 +1,67 @@ +prophesize(MSUserAbsenceReaderInterface::class); + $userAbsenceReader->isUserAbsent($user)->willReturn($absenceFromMicrosoft); + + $clock = new MockClock(new \DateTimeImmutable('2023-07-01T12:00:00')); + + $syncer = new MSUserAbsenceSync($userAbsenceReader->reveal(), $clock); + + $syncer->syncUserAbsence($user); + + self::assertEquals($expectedAbsence, $user->isAbsent(), $message); + self::assertEquals($expectedAbsenceStart, $user->getAbsenceStart(), $message); + } + + public function provideDataTestSyncUserAbsence(): iterable + { + yield [new User(), false, false, null, "user present remains present"]; + yield [new User(), true, true, new \DateTimeImmutable('2023-07-01T12:00:00'), "user present becomes absent"]; + + $user = new User(); + $user->setAbsenceStart($abs = new \DateTimeImmutable("2023-07-01T12:00:00")); + yield [$user, true, true, $abs, "user absent remains absent"]; + + $user = new User(); + $user->setAbsenceStart($abs = new \DateTimeImmutable("2023-07-01T12:00:00")); + yield [$user, false, false, null, "user absent becomes present"]; + + yield [new User(), null, false, null, "user not syncable: presence do not change"]; + + $user = new User(); + $user->setAbsenceStart($abs = new \DateTimeImmutable("2023-07-01T12:00:00")); + yield [$user, null, true, $abs, "user not syncable: absence do not change"]; + } +} From 77d4b13c1bab2624a9b28c016f522486f859abb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 6 Jul 2023 21:33:01 +0200 Subject: [PATCH 20/50] Sync user absence / presence within MapAndSubscribeUserCalendarCommand --- .../MapAndSubscribeUserCalendarCommand.php | 182 ++++++++++-------- .../MSGraph/MSGraphUserRepository.php | 2 +- .../Connector/MSGraph/MSUserAbsenceReader.php | 3 +- .../Connector/MSGraph/MSUserAbsenceSync.php | 6 + .../MSGraph/MSUserAbsenceSyncTest.php | 3 +- 5 files changed, 109 insertions(+), 87 deletions(-) diff --git a/src/Bundle/ChillCalendarBundle/Command/MapAndSubscribeUserCalendarCommand.php b/src/Bundle/ChillCalendarBundle/Command/MapAndSubscribeUserCalendarCommand.php index b90fb83d4..193b04934 100644 --- a/src/Bundle/ChillCalendarBundle/Command/MapAndSubscribeUserCalendarCommand.php +++ b/src/Bundle/ChillCalendarBundle/Command/MapAndSubscribeUserCalendarCommand.php @@ -18,9 +18,12 @@ declare(strict_types=1); namespace Chill\CalendarBundle\Command; +use Chill\CalendarBundle\Exception\UserAbsenceSyncException; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\EventsOnUserSubscriptionCreator; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MapCalendarToUser; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MSGraphUserRepository; +use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MSUserAbsenceSync; +use Chill\MainBundle\Repository\UserRepositoryInterface; use DateInterval; use DateTimeImmutable; use Doctrine\ORM\EntityManagerInterface; @@ -30,32 +33,17 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -class MapAndSubscribeUserCalendarCommand extends Command +final class MapAndSubscribeUserCalendarCommand extends Command { - private EntityManagerInterface $em; - - private EventsOnUserSubscriptionCreator $eventsOnUserSubscriptionCreator; - - private LoggerInterface $logger; - - private MapCalendarToUser $mapCalendarToUser; - - private MSGraphUserRepository $userRepository; - public function __construct( - EntityManagerInterface $em, - EventsOnUserSubscriptionCreator $eventsOnUserSubscriptionCreator, - LoggerInterface $logger, - MapCalendarToUser $mapCalendarToUser, - MSGraphUserRepository $userRepository + private readonly EntityManagerInterface $em, + private readonly EventsOnUserSubscriptionCreator $eventsOnUserSubscriptionCreator, + private readonly LoggerInterface $logger, + private readonly MapCalendarToUser $mapCalendarToUser, + private readonly UserRepositoryInterface $userRepository, + private readonly MSUserAbsenceSync $userAbsenceSync, ) { parent::__construct('chill:calendar:msgraph-user-map-subscribe'); - - $this->em = $em; - $this->eventsOnUserSubscriptionCreator = $eventsOnUserSubscriptionCreator; - $this->logger = $logger; - $this->mapCalendarToUser = $mapCalendarToUser; - $this->userRepository = $userRepository; } public function execute(InputInterface $input, OutputInterface $output): int @@ -67,83 +55,109 @@ class MapAndSubscribeUserCalendarCommand extends Command /** @var DateInterval $interval the interval before the end of the expiration */ $interval = new DateInterval('P1D'); $expiration = (new DateTimeImmutable('now'))->add(new DateInterval($input->getOption('subscription-duration'))); - $total = $this->userRepository->countByMostOldSubscriptionOrWithoutSubscriptionOrData($interval); + $users = $this->userRepository->findAllAsArray('fr'); $created = 0; $renewed = 0; - $this->logger->info(self::class . ' the number of user to get - renew', [ - 'total' => $total, + $this->logger->info(self::class . ' start user to get - renew', [ 'expiration' => $expiration->format(DateTimeImmutable::ATOM), ]); - while ($offset < $total) { - $users = $this->userRepository->findByMostOldSubscriptionOrWithoutSubscriptionOrData( - $interval, - $limit, - $offset - ); + foreach ($users as $u) { + ++$offset; - foreach ($users as $user) { - if (!$this->mapCalendarToUser->hasUserId($user)) { - $this->mapCalendarToUser->writeMetadata($user); - } - - if ($this->mapCalendarToUser->hasUserId($user)) { - // we first try to renew an existing subscription, if any. - // if not, or if it fails, we try to create a new one - if ($this->mapCalendarToUser->hasActiveSubscription($user)) { - $this->logger->debug(self::class . ' renew a subscription for', [ - 'userId' => $user->getId(), - 'username' => $user->getUsernameCanonical(), - ]); - - ['secret' => $secret, 'id' => $id, 'expiration' => $expirationTs] - = $this->eventsOnUserSubscriptionCreator->renewSubscriptionForUser($user, $expiration); - $this->mapCalendarToUser->writeSubscriptionMetadata($user, $expirationTs, $id, $secret); - - if (0 !== $expirationTs) { - ++$renewed; - } else { - $this->logger->warning(self::class . ' could not renew subscription for a user', [ - 'userId' => $user->getId(), - 'username' => $user->getUsernameCanonical(), - ]); - } - } - - if (!$this->mapCalendarToUser->hasActiveSubscription($user)) { - $this->logger->debug(self::class . ' create a subscription for', [ - 'userId' => $user->getId(), - 'username' => $user->getUsernameCanonical(), - ]); - - ['secret' => $secret, 'id' => $id, 'expiration' => $expirationTs] - = $this->eventsOnUserSubscriptionCreator->createSubscriptionForUser($user, $expiration); - $this->mapCalendarToUser->writeSubscriptionMetadata($user, $expirationTs, $id, $secret); - - if (0 !== $expirationTs) { - ++$created; - } else { - $this->logger->warning(self::class . ' could not create subscription for a user', [ - 'userId' => $user->getId(), - 'username' => $user->getUsernameCanonical(), - ]); - } - } - } - - ++$offset; + if (false === $u['enabled']) { + continue; } - $this->em->flush(); - $this->em->clear(); + $user = $this->userRepository->find($u['id']); + + if (null === $user) { + $this->logger->error("could not find user by id", ['uid' => $u['id']]); + $output->writeln("could not find user by id : " . $u['id']); + continue; + } + + if (!$this->mapCalendarToUser->hasUserId($user)) { + $user = $this->mapCalendarToUser->writeMetadata($user); + + // if user still does not have userid, continue + if (!$this->mapCalendarToUser->hasUserId($user)) { + $this->logger->warning("user does not have a counterpart on ms api", ['userId' => $user->getId(), 'email' => $user->getEmail()]); + $output->writeln(sprintf("giving up for user with email %s and id %s", $user->getEmail(), $user->getId())); + + continue; + } + } + + // sync user absence + try { + $this->userAbsenceSync->syncUserAbsence($user); + } catch (UserAbsenceSyncException $e) { + $this->logger->error("could not sync user absence", ['userId' => $user->getId(), 'email' => $user->getEmail(), 'exception' => $e->getTraceAsString(), "message" => $e->getMessage()]); + $output->writeln(sprintf("Could not sync user absence: id: %s and email: %s", $user->getId(), $user->getEmail())); + throw $e; + } + + // we first try to renew an existing subscription, if any. + // if not, or if it fails, we try to create a new one + if ($this->mapCalendarToUser->hasActiveSubscription($user)) { + $this->logger->debug(self::class . ' renew a subscription for', [ + 'userId' => $user->getId(), + 'username' => $user->getUsernameCanonical(), + ]); + + ['secret' => $secret, 'id' => $id, 'expiration' => $expirationTs] + = $this->eventsOnUserSubscriptionCreator->renewSubscriptionForUser($user, $expiration); + $this->mapCalendarToUser->writeSubscriptionMetadata($user, $expirationTs, $id, $secret); + + if (0 !== $expirationTs) { + ++$renewed; + } else { + $this->logger->warning(self::class . ' could not renew subscription for a user', [ + 'userId' => $user->getId(), + 'username' => $user->getUsernameCanonical(), + ]); + } + } + + if (!$this->mapCalendarToUser->hasActiveSubscription($user)) { + $this->logger->debug(self::class . ' create a subscription for', [ + 'userId' => $user->getId(), + 'username' => $user->getUsernameCanonical(), + ]); + + ['secret' => $secret, 'id' => $id, 'expiration' => $expirationTs] + = $this->eventsOnUserSubscriptionCreator->createSubscriptionForUser($user, $expiration); + $this->mapCalendarToUser->writeSubscriptionMetadata($user, $expirationTs, $id, $secret); + + if (0 !== $expirationTs) { + ++$created; + } else { + $this->logger->warning(self::class . ' could not create subscription for a user', [ + 'userId' => $user->getId(), + 'username' => $user->getUsernameCanonical(), + ]); + } + } + + + if (0 === $offset % $limit) { + $this->em->flush(); + $this->em->clear(); + } } + $this->em->flush(); + $this->em->clear(); + $this->logger->warning(self::class . ' process executed', [ 'created' => $created, 'renewed' => $renewed, ]); + $output->writeln("users synchronized"); + return 0; } @@ -152,7 +166,7 @@ class MapAndSubscribeUserCalendarCommand extends Command parent::configure(); $this - ->setDescription('MSGraph: collect user metadata and create subscription on events for users') + ->setDescription('MSGraph: collect user metadata and create subscription on events for users, and sync the user absence-presence') ->addOption( 'renew-before-end-interval', 'r', diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSGraphUserRepository.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSGraphUserRepository.php index c523a1e92..ae822669c 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSGraphUserRepository.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSGraphUserRepository.php @@ -65,7 +65,7 @@ class MSGraphUserRepository } /** - * @return array|User[] + * @return array */ public function findByMostOldSubscriptionOrWithoutSubscriptionOrData(DateInterval $interval, int $limit = 50, int $offset = 0): array { diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php index 83376528f..0e756c194 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php @@ -13,6 +13,7 @@ namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph; use Chill\CalendarBundle\Exception\UserAbsenceSyncException; use Chill\MainBundle\Entity\User; +use Psr\Log\LoggerInterface; use Symfony\Component\Clock\ClockInterface; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; @@ -43,7 +44,7 @@ final readonly class MSUserAbsenceReader implements MSUserAbsenceReaderInterface try { $automaticRepliesSettings = $this->machineHttpClient - ->request('GET', '/users/' . $id . '/mailboxSettings/automaticRepliesSetting') + ->request('GET', 'users/' . $id . '/mailboxSettings/automaticRepliesSetting') ->toArray(true); } catch (ClientExceptionInterface|DecodingExceptionInterface|RedirectionExceptionInterface|TransportExceptionInterface $e) { throw new UserAbsenceSyncException("Error receiving response for mailboxSettings", 0, $e); diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSync.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSync.php index aaa06a64b..10bf21b9b 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSync.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSync.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph; use Chill\MainBundle\Entity\User; +use Psr\Log\LoggerInterface; use Symfony\Component\Clock\ClockInterface; readonly class MSUserAbsenceSync @@ -19,6 +20,7 @@ readonly class MSUserAbsenceSync public function __construct( private MSUserAbsenceReaderInterface $absenceReader, private ClockInterface $clock, + private LoggerInterface $logger, ) { } @@ -35,9 +37,13 @@ readonly class MSUserAbsenceSync return; } + $this->logger->info("will change user absence", ['userId' => $user->getId()]); + if ($absence) { + $this->logger->debug("make user absent", ['userId' => $user->getId()]); $user->setAbsenceStart($this->clock->now()); } else { + $this->logger->debug("make user present", ['userId' => $user->getId()]); $user->setAbsenceStart(null); } } diff --git a/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSyncTest.php b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSyncTest.php index ddd92086d..1b0f1e416 100644 --- a/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSyncTest.php +++ b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSyncTest.php @@ -17,6 +17,7 @@ use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MSUserAbsenceSync; use Chill\MainBundle\Entity\User; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; +use Psr\Log\NullLogger; use Symfony\Component\Clock\MockClock; /** @@ -37,7 +38,7 @@ class MSUserAbsenceSyncTest extends TestCase $clock = new MockClock(new \DateTimeImmutable('2023-07-01T12:00:00')); - $syncer = new MSUserAbsenceSync($userAbsenceReader->reveal(), $clock); + $syncer = new MSUserAbsenceSync($userAbsenceReader->reveal(), $clock, new NullLogger()); $syncer->syncUserAbsence($user); From 9b6e6ec20f6bf425245de6f1f08a21e46b7d4eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 6 Jul 2023 21:34:43 +0200 Subject: [PATCH 21/50] add a changie --- .changes/unreleased/Feature-20230706-213428.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changes/unreleased/Feature-20230706-213428.yaml diff --git a/.changes/unreleased/Feature-20230706-213428.yaml b/.changes/unreleased/Feature-20230706-213428.yaml new file mode 100644 index 000000000..092e5a2e0 --- /dev/null +++ b/.changes/unreleased/Feature-20230706-213428.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: 'Sync user absence / presence through microsoft outlook / graph api. ' +time: 2023-07-06T21:34:28.973144334+02:00 +custom: + Issue: "124" From 93a598b5497339590a4936b5dae4036082d04de6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 6 Jul 2023 21:45:29 +0200 Subject: [PATCH 22/50] improve php applying rector rules --- .../ChillCalendarBundle/Exception/UserAbsenceSyncException.php | 2 +- .../RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php b/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php index 558774fc5..a5e5a679a 100644 --- a/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php +++ b/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php @@ -13,7 +13,7 @@ namespace Chill\CalendarBundle\Exception; class UserAbsenceSyncException extends \LogicException { - public function __construct(string $message = "", int $code = 20230706, ?\Throwable $previous = null) + public function __construct(string $message = "", int $code = 20_230_706, ?\Throwable $previous = null) { parent::__construct($message, $code, $previous); } diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php index 0e756c194..c70072a47 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php @@ -53,7 +53,7 @@ final readonly class MSUserAbsenceReader implements MSUserAbsenceReaderInterface } if (!array_key_exists("status", $automaticRepliesSettings)) { - throw new \LogicException("no key \"status\" on automatic replies settings: " . json_encode($automaticRepliesSettings)); + throw new \LogicException("no key \"status\" on automatic replies settings: " . json_encode($automaticRepliesSettings, JSON_THROW_ON_ERROR)); } return match ($automaticRepliesSettings['status']) { From d3251075e978331ed9b003d223e83bd95f2f8fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 6 Jul 2023 21:55:29 +0200 Subject: [PATCH 23/50] fix loading of kernel if ms calendar is not created --- .../MSGraph/MSGraphUserRepository.php | 84 ------------------- .../RemoteCalendarCompilerPass.php | 16 ++-- 2 files changed, 6 insertions(+), 94 deletions(-) delete mode 100644 src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSGraphUserRepository.php diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSGraphUserRepository.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSGraphUserRepository.php deleted file mode 100644 index ae822669c..000000000 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSGraphUserRepository.php +++ /dev/null @@ -1,84 +0,0 @@ -'msgraph' ?? 'subscription_events_expiration' - OR (attributes->'msgraph' ?? 'subscription_events_expiration' AND (attributes->'msgraph'->>'subscription_events_expiration')::int < EXTRACT(EPOCH FROM (NOW() + :interval::interval))) - LIMIT :limit OFFSET :offset - ; - SQL; - - private EntityManagerInterface $entityManager; - - public function __construct(EntityManagerInterface $entityManager) - { - $this->entityManager = $entityManager; - } - - public function countByMostOldSubscriptionOrWithoutSubscriptionOrData(DateInterval $interval): int - { - $rsm = new ResultSetMapping(); - $rsm->addScalarResult('c', 'c'); - - $sql = strtr(self::MOST_OLD_SUBSCRIPTION_OR_ANY_MS_GRAPH, [ - '{select}' => 'COUNT(u) AS c', - 'LIMIT :limit OFFSET :offset' => '', - ]); - - return $this->entityManager->createNativeQuery($sql, $rsm)->setParameters([ - 'interval' => $interval, - ])->getSingleScalarResult(); - } - - /** - * @return array - */ - public function findByMostOldSubscriptionOrWithoutSubscriptionOrData(DateInterval $interval, int $limit = 50, int $offset = 0): array - { - $rsm = new ResultSetMappingBuilder($this->entityManager); - $rsm->addRootEntityFromClassMetadata(User::class, 'u'); - - return $this->entityManager->createNativeQuery( - strtr(self::MOST_OLD_SUBSCRIPTION_OR_ANY_MS_GRAPH, ['{select}' => $rsm->generateSelectClause()]), - $rsm - )->setParameters([ - 'interval' => $interval, - 'limit' => $limit, - 'offset' => $offset, - ])->getResult(); - } -} diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php index f56735de7..6a986c9db 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php @@ -23,6 +23,8 @@ use Chill\CalendarBundle\Command\MapAndSubscribeUserCalendarCommand; use Chill\CalendarBundle\Controller\RemoteCalendarConnectAzureController; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MachineHttpClient; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MachineTokenStorage; +use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MSUserAbsenceReaderInterface; +use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MSUserAbsenceSync; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraphRemoteCalendarConnector; use Chill\CalendarBundle\RemoteCalendar\Connector\NullRemoteCalendarConnector; use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface; @@ -37,17 +39,13 @@ class RemoteCalendarCompilerPass implements CompilerPassInterface public function process(ContainerBuilder $container) { $config = $container->getParameter('chill_calendar'); - $connector = null; - if (!$config['remote_calendars_sync']['enabled']) { - $connector = NullRemoteCalendarConnector::class; - } - - if ($config['remote_calendars_sync']['microsoft_graph']['enabled']) { + if (true === $config['remote_calendars_sync']['microsoft_graph']['enabled']) { $connector = MSGraphRemoteCalendarConnector::class; $container->setAlias(HttpClientInterface::class . ' $machineHttpClient', MachineHttpClient::class); } else { + $connector = NullRemoteCalendarConnector::class; // remove services which cannot be loaded $container->removeDefinition(MapAndSubscribeUserCalendarCommand::class); $container->removeDefinition(AzureGrantAdminConsentAndAcquireToken::class); @@ -55,16 +53,14 @@ class RemoteCalendarCompilerPass implements CompilerPassInterface $container->removeDefinition(MachineTokenStorage::class); $container->removeDefinition(MachineHttpClient::class); $container->removeDefinition(MSGraphRemoteCalendarConnector::class); + $container->removeDefinition(MSUserAbsenceReaderInterface::class); + $container->removeDefinition(MSUserAbsenceSync::class); } if (!$container->hasAlias(Azure::class) && $container->hasDefinition('knpu.oauth2.client.azure')) { $container->setAlias(Azure::class, 'knpu.oauth2.provider.azure'); } - if (null === $connector) { - throw new RuntimeException('Could not configure remote calendar'); - } - foreach ([ NullRemoteCalendarConnector::class, MSGraphRemoteCalendarConnector::class, ] as $serviceId) { From 43b7139488dd0f3b99286b421ba5c1594b709d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 6 Jul 2023 22:01:45 +0200 Subject: [PATCH 24/50] One more changie [ci-skip] --- .changes/unreleased/Fixed-20230706-220125.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changes/unreleased/Fixed-20230706-220125.yaml diff --git a/.changes/unreleased/Fixed-20230706-220125.yaml b/.changes/unreleased/Fixed-20230706-220125.yaml new file mode 100644 index 000000000..b9449d514 --- /dev/null +++ b/.changes/unreleased/Fixed-20230706-220125.yaml @@ -0,0 +1,6 @@ +kind: Fixed +body: 'Command to subscribe on MS Graph users calendars: improve the loop to be more + efficient' +time: 2023-07-06T22:01:25.847374805+02:00 +custom: + Issue: "" From 7ccff61c254cea360b68bf33d74ea0988ee1a5e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 7 Jul 2023 09:17:36 +0200 Subject: [PATCH 25/50] Refactor ListAccompanyingPeriod to use a helper for most of the work --- .../Export/Export/ListAccompanyingPeriod.php | 313 +----------------- .../Helper/ListAccompanyingPeriodHelper.php | 306 +++++++++++++++++ .../translations/messages.fr.yml | 2 +- 3 files changed, 315 insertions(+), 306 deletions(-) create mode 100644 src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriod.php index af66ab312..3f7821bbc 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriod.php @@ -29,6 +29,7 @@ use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress; use Chill\PersonBundle\Entity\Person\PersonCenterHistory; use Chill\PersonBundle\Entity\SocialWork\SocialIssue; use Chill\PersonBundle\Export\Declarations; +use Chill\PersonBundle\Export\Helper\ListAccompanyingPeriodHelper; use Chill\PersonBundle\Repository\PersonRepository; use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository; use Chill\PersonBundle\Security\Authorization\PersonVoter; @@ -45,95 +46,13 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Contracts\Translation\TranslatorInterface; use function strlen; -class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface +final readonly class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface { - private const FIELDS = [ - 'id', - 'step', - 'stepSince', - 'openingDate', - 'closingDate', - 'referrer', - 'referrerSince', - 'administrativeLocation', - 'locationIsPerson', - 'locationIsTemp', - 'locationPersonName', - 'locationPersonId', - 'origin', - 'closingMotive', - 'confidential', - 'emergency', - 'intensity', - 'job', - 'isRequestorPerson', - 'isRequestorThirdParty', - 'requestorPerson', - 'requestorPersonId', - 'requestorThirdParty', - 'requestorThirdPartyId', - 'scopes', - 'socialIssues', - 'createdAt', - 'createdBy', - 'updatedAt', - 'updatedBy', - ]; - - private ExportAddressHelper $addressHelper; - - private DateTimeHelper $dateTimeHelper; - - private EntityManagerInterface $entityManager; - - private PersonRenderInterface $personRender; - - private PersonRepository $personRepository; - - private RollingDateConverterInterface $rollingDateConverter; - - private SocialIssueRender $socialIssueRender; - - private SocialIssueRepository $socialIssueRepository; - - private ThirdPartyRender $thirdPartyRender; - - private ThirdPartyRepository $thirdPartyRepository; - - private TranslatableStringHelperInterface $translatableStringHelper; - - private TranslatorInterface $translator; - - private UserHelper $userHelper; - public function __construct( - ExportAddressHelper $addressHelper, - DateTimeHelper $dateTimeHelper, - EntityManagerInterface $entityManager, - PersonRenderInterface $personRender, - PersonRepository $personRepository, - ThirdPartyRepository $thirdPartyRepository, - ThirdPartyRender $thirdPartyRender, - SocialIssueRepository $socialIssueRepository, - SocialIssueRender $socialIssueRender, - TranslatableStringHelperInterface $translatableStringHelper, - TranslatorInterface $translator, - RollingDateConverterInterface $rollingDateConverter, - UserHelper $userHelper + private EntityManagerInterface $entityManager, + private RollingDateConverterInterface $rollingDateConverter, + private ListAccompanyingPeriodHelper $listAccompanyingPeriodHelper, ) { - $this->addressHelper = $addressHelper; - $this->dateTimeHelper = $dateTimeHelper; - $this->entityManager = $entityManager; - $this->personRender = $personRender; - $this->personRepository = $personRepository; - $this->socialIssueRender = $socialIssueRender; - $this->socialIssueRepository = $socialIssueRepository; - $this->thirdPartyRender = $thirdPartyRender; - $this->thirdPartyRepository = $thirdPartyRepository; - $this->translatableStringHelper = $translatableStringHelper; - $this->translator = $translator; - $this->rollingDateConverter = $rollingDateConverter; - $this->userHelper = $userHelper; } public function buildForm(FormBuilderInterface $builder) @@ -169,141 +88,12 @@ class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface public function getLabels($key, array $values, $data) { - if (substr($key, 0, strlen('address_fields')) === 'address_fields') { - return $this->addressHelper->getLabel($key, $values, $data, 'address_fields'); - } - - switch ($key) { - case 'stepSince': - case 'openingDate': - case 'closingDate': - case 'referrerSince': - case 'createdAt': - case 'updatedAt': - return $this->dateTimeHelper->getLabel('export.list.acp.' . $key); - - case 'origin': - case 'closingMotive': - case 'job': - return function ($value) use ($key) { - if ('_header' === $value) { - return 'export.list.acp.' . $key; - } - - if (null === $value) { - return ''; - } - - return $this->translatableStringHelper->localize(json_decode($value, true, 512, JSON_THROW_ON_ERROR)); - }; - - case 'locationPersonName': - case 'requestorPerson': - return function ($value) use ($key) { - if ('_header' === $value) { - return 'export.list.acp.' . $key; - } - - if (null === $value || null === $person = $this->personRepository->find($value)) { - return ''; - } - - return $this->personRender->renderString($person, []); - }; - - case 'requestorThirdParty': - return function ($value) use ($key) { - if ('_header' === $value) { - return 'export.list.acp.' . $key; - } - - if (null === $value || null === $thirdparty = $this->thirdPartyRepository->find($value)) { - return ''; - } - - return $this->thirdPartyRender->renderString($thirdparty, []); - }; - - case 'scopes': - return function ($value) use ($key) { - if ('_header' === $value) { - return 'export.list.acp.' . $key; - } - - if (null === $value) { - return ''; - } - - return implode( - '|', - array_map( - fn ($s) => $this->translatableStringHelper->localize($s), - json_decode($value, true, 512, JSON_THROW_ON_ERROR) - ) - ); - }; - - case 'socialIssues': - return function ($value) use ($key) { - if ('_header' === $value) { - return 'export.list.acp.' . $key; - } - - if (null === $value) { - return ''; - } - - return implode( - '|', - array_map( - fn ($s) => $this->socialIssueRender->renderString($this->socialIssueRepository->find($s), []), - json_decode($value, true, 512, JSON_THROW_ON_ERROR) - ) - ); - }; - - case 'step': - return fn ($value) => match ($value) { - '_header' => 'export.list.acp.step', - null => '', - AccompanyingPeriod::STEP_DRAFT => $this->translator->trans('course.draft'), - AccompanyingPeriod::STEP_CONFIRMED => $this->translator->trans('course.confirmed'), - AccompanyingPeriod::STEP_CLOSED => $this->translator->trans('course.closed'), - AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT => $this->translator->trans('course.inactive_short'), - AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_LONG => $this->translator->trans('course.inactive_long'), - default => $value, - }; - - case 'intensity': - return fn ($value) => match ($value) { - '_header' => 'export.list.acp.intensity', - null => '', - AccompanyingPeriod::INTENSITY_OCCASIONAL => $this->translator->trans('occasional'), - AccompanyingPeriod::INTENSITY_REGULAR => $this->translator->trans('regular'), - default => $value, - }; - - default: - return static function ($value) use ($key) { - if ('_header' === $value) { - return 'export.list.acp.' . $key; - } - - if (null === $value) { - return ''; - } - - return $value; - }; - } + return $this->listAccompanyingPeriodHelper->getLabels($key, $values, $data); } public function getQueryKeys($data) { - return array_merge( - self::FIELDS, - $this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields') - ); + return $this->listAccompanyingPeriodHelper->getQueryKeys($data); } public function getResult($query, $data) @@ -341,7 +131,7 @@ class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface ->setParameter('list_acp_step', AccompanyingPeriod::STEP_DRAFT) ->setParameter('authorized_centers', $centers); - $this->addSelectClauses($qb, $this->rollingDateConverter->convert($data['calc_date'])); + $this->listAccompanyingPeriodHelper->addSelectClauses($qb, $this->rollingDateConverter->convert($data['calc_date'])); return $qb; } @@ -357,91 +147,4 @@ class ListAccompanyingPeriod implements ListInterface, GroupedExportInterface Declarations::ACP_TYPE, ]; } - - private function addSelectClauses(QueryBuilder $qb, DateTimeImmutable $calcDate): void - { - // add the regular fields - foreach (['id', 'openingDate', 'closingDate', 'confidential', 'emergency', 'intensity', 'createdAt', 'updatedAt'] as $field) { - $qb->addSelect(sprintf('acp.%s AS %s', $field, $field)); - } - - // add the field which are simple association - foreach (['origin' => 'label', 'closingMotive' => 'name', 'job' => 'label', 'createdBy' => 'label', 'updatedBy' => 'label', 'administrativeLocation' => 'name'] as $entity => $field) { - $qb - ->leftJoin(sprintf('acp.%s', $entity), "{$entity}_t") - ->addSelect(sprintf('%s_t.%s AS %s', $entity, $field, $entity)); - } - - // step at date - $qb - ->addSelect('stepHistory.step AS step') - ->addSelect('stepHistory.startDate AS stepSince') - ->leftJoin('acp.stepHistories', 'stepHistory') - ->andWhere( - $qb->expr()->andX( - $qb->expr()->lte('stepHistory.startDate', ':calcDate'), - $qb->expr()->orX($qb->expr()->isNull('stepHistory.endDate'), $qb->expr()->gt('stepHistory.endDate', ':calcDate')) - ) - ); - - // referree at date - $qb - ->addSelect('referrer_t.label AS referrer') - ->addSelect('userHistory.startDate AS referrerSince') - ->leftJoin('acp.userHistories', 'userHistory') - ->leftJoin('userHistory.user', 'referrer_t') - ->andWhere( - $qb->expr()->orX( - $qb->expr()->isNull('userHistory'), - $qb->expr()->andX( - $qb->expr()->lte('userHistory.startDate', ':calcDate'), - $qb->expr()->orX($qb->expr()->isNull('userHistory.endDate'), $qb->expr()->gt('userHistory.endDate', ':calcDate')) - ) - ) - ); - - // location of the acp - $qb - ->addSelect('CASE WHEN locationHistory.personLocation IS NOT NULL THEN 1 ELSE 0 END AS locationIsPerson') - ->addSelect('CASE WHEN locationHistory.personLocation IS NOT NULL THEN 0 ELSE 1 END AS locationIsTemp') - ->addSelect('IDENTITY(locationHistory.personLocation) AS locationPersonName') - ->addSelect('IDENTITY(locationHistory.personLocation) AS locationPersonId') - ->leftJoin('acp.locationHistories', 'locationHistory') - ->andWhere( - $qb->expr()->orX( - $qb->expr()->isNull('locationHistory'), - $qb->expr()->andX( - $qb->expr()->lte('locationHistory.startDate', ':calcDate'), - $qb->expr()->orX($qb->expr()->isNull('locationHistory.endDate'), $qb->expr()->gt('locationHistory.endDate', ':calcDate')) - ) - ) - ) - ->leftJoin( - PersonHouseholdAddress::class, - 'personAddress', - Join::WITH, - 'locationHistory.personLocation = personAddress.person AND (personAddress.validFrom <= :calcDate AND (personAddress.validTo IS NULL OR personAddress.validTo > :calcDate))' - ) - ->leftJoin(Address::class, 'acp_address', Join::WITH, 'COALESCE(IDENTITY(locationHistory.addressLocation), IDENTITY(personAddress.address)) = acp_address.id'); - - $this->addressHelper->addSelectClauses(ExportAddressHelper::F_ALL, $qb, 'acp_address', 'address_fields'); - - // requestor - $qb - ->addSelect('CASE WHEN acp.requestorPerson IS NULL THEN 1 ELSE 0 END AS isRequestorPerson') - ->addSelect('CASE WHEN acp.requestorPerson IS NULL THEN 0 ELSE 1 END AS isRequestorThirdParty') - ->addSelect('IDENTITY(acp.requestorPerson) AS requestorPersonId') - ->addSelect('IDENTITY(acp.requestorThirdParty) AS requestorThirdPartyId') - ->addSelect('IDENTITY(acp.requestorPerson) AS requestorPerson') - ->addSelect('IDENTITY(acp.requestorThirdParty) AS requestorThirdParty'); - - $qb - // scopes - ->addSelect('(SELECT AGGREGATE(scope.name) FROM ' . Scope::class . ' scope WHERE scope MEMBER OF acp.scopes) AS scopes') - // social issues - ->addSelect('(SELECT AGGREGATE(socialIssue.id) FROM ' . SocialIssue::class . ' socialIssue WHERE socialIssue MEMBER OF acp.socialIssues) AS socialIssues'); - - // add parameter - $qb->setParameter('calcDate', $calcDate); - } } diff --git a/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php b/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php new file mode 100644 index 000000000..8671b4977 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php @@ -0,0 +1,306 @@ +addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields') + ); + } + + public function getLabels($key, array $values, $data) + { + if (substr($key, 0, strlen('address_fields')) === 'address_fields') { + return $this->addressHelper->getLabel($key, $values, $data, 'address_fields'); + } + + switch ($key) { + case 'stepSince': + case 'openingDate': + case 'closingDate': + case 'referrerSince': + case 'createdAt': + case 'updatedAt': + return $this->dateTimeHelper->getLabel('export.list.acp.' . $key); + + case 'origin': + case 'closingMotive': + case 'job': + return function ($value) use ($key) { + if ('_header' === $value) { + return 'export.list.acp.' . $key; + } + + if (null === $value) { + return ''; + } + + return $this->translatableStringHelper->localize(json_decode($value, true, 512, JSON_THROW_ON_ERROR)); + }; + + case 'locationPersonName': + case 'requestorPerson': + return function ($value) use ($key) { + if ('_header' === $value) { + return 'export.list.acp.' . $key; + } + + if (null === $value || null === $person = $this->personRepository->find($value)) { + return ''; + } + + return $this->personRender->renderString($person, []); + }; + + case 'requestorThirdParty': + return function ($value) use ($key) { + if ('_header' === $value) { + return 'export.list.acp.' . $key; + } + + if (null === $value || null === $thirdparty = $this->thirdPartyRepository->find($value)) { + return ''; + } + + return $this->thirdPartyRender->renderString($thirdparty, []); + }; + + case 'scopes': + return function ($value) use ($key) { + if ('_header' === $value) { + return 'export.list.acp.' . $key; + } + + if (null === $value) { + return ''; + } + + return implode( + '|', + array_map( + fn ($s) => $this->translatableStringHelper->localize($s), + json_decode($value, true, 512, JSON_THROW_ON_ERROR) + ) + ); + }; + + case 'socialIssues': + return function ($value) use ($key) { + if ('_header' === $value) { + return 'export.list.acp.' . $key; + } + + if (null === $value) { + return ''; + } + + return implode( + '|', + array_map( + fn ($s) => $this->socialIssueRender->renderString($this->socialIssueRepository->find($s), []), + json_decode($value, true, 512, JSON_THROW_ON_ERROR) + ) + ); + }; + + case 'step': + return fn ($value) => match ($value) { + '_header' => 'export.list.acp.step', + null => '', + AccompanyingPeriod::STEP_DRAFT => $this->translator->trans('course.draft'), + AccompanyingPeriod::STEP_CONFIRMED => $this->translator->trans('course.confirmed'), + AccompanyingPeriod::STEP_CLOSED => $this->translator->trans('course.closed'), + AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_SHORT => $this->translator->trans('course.inactive_short'), + AccompanyingPeriod::STEP_CONFIRMED_INACTIVE_LONG => $this->translator->trans('course.inactive_long'), + default => $value, + }; + + case 'intensity': + return fn ($value) => match ($value) { + '_header' => 'export.list.acp.intensity', + null => '', + AccompanyingPeriod::INTENSITY_OCCASIONAL => $this->translator->trans('occasional'), + AccompanyingPeriod::INTENSITY_REGULAR => $this->translator->trans('regular'), + default => $value, + }; + + default: + return static function ($value) use ($key) { + if ('_header' === $value) { + return 'export.list.acp.' . $key; + } + + if (null === $value) { + return ''; + } + + return $value; + }; + } + } + + public function addSelectClauses(QueryBuilder $qb, \DateTimeImmutable $calcDate): void + { + // add the regular fields + foreach (['id', 'openingDate', 'closingDate', 'confidential', 'emergency', 'intensity', 'createdAt', 'updatedAt'] as $field) { + $qb->addSelect(sprintf('acp.%s AS %s', $field, $field)); + } + + // add the field which are simple association + foreach (['origin' => 'label', 'closingMotive' => 'name', 'job' => 'label', 'createdBy' => 'label', 'updatedBy' => 'label', 'administrativeLocation' => 'name'] as $entity => $field) { + $qb + ->leftJoin(sprintf('acp.%s', $entity), "{$entity}_t") + ->addSelect(sprintf('%s_t.%s AS %s', $entity, $field, $entity)); + } + + // step at date + $qb + ->addSelect('stepHistory.step AS step') + ->addSelect('stepHistory.startDate AS stepSince') + ->leftJoin('acp.stepHistories', 'stepHistory') + ->andWhere( + $qb->expr()->andX( + $qb->expr()->lte('stepHistory.startDate', ':calcDate'), + $qb->expr()->orX($qb->expr()->isNull('stepHistory.endDate'), $qb->expr()->gt('stepHistory.endDate', ':calcDate')) + ) + ); + + // referree at date + $qb + ->addSelect('referrer_t.label AS referrer') + ->addSelect('userHistory.startDate AS referrerSince') + ->leftJoin('acp.userHistories', 'userHistory') + ->leftJoin('userHistory.user', 'referrer_t') + ->andWhere( + $qb->expr()->orX( + $qb->expr()->isNull('userHistory'), + $qb->expr()->andX( + $qb->expr()->lte('userHistory.startDate', ':calcDate'), + $qb->expr()->orX($qb->expr()->isNull('userHistory.endDate'), $qb->expr()->gt('userHistory.endDate', ':calcDate')) + ) + ) + ); + + // location of the acp + $qb + ->addSelect('CASE WHEN locationHistory.personLocation IS NOT NULL THEN 1 ELSE 0 END AS locationIsPerson') + ->addSelect('CASE WHEN locationHistory.personLocation IS NOT NULL THEN 0 ELSE 1 END AS locationIsTemp') + ->addSelect('IDENTITY(locationHistory.personLocation) AS locationPersonName') + ->addSelect('IDENTITY(locationHistory.personLocation) AS locationPersonId') + ->leftJoin('acp.locationHistories', 'locationHistory') + ->andWhere( + $qb->expr()->orX( + $qb->expr()->isNull('locationHistory'), + $qb->expr()->andX( + $qb->expr()->lte('locationHistory.startDate', ':calcDate'), + $qb->expr()->orX($qb->expr()->isNull('locationHistory.endDate'), $qb->expr()->gt('locationHistory.endDate', ':calcDate')) + ) + ) + ) + ->leftJoin( + PersonHouseholdAddress::class, + 'personAddress', + Join::WITH, + 'locationHistory.personLocation = personAddress.person AND (personAddress.validFrom <= :calcDate AND (personAddress.validTo IS NULL OR personAddress.validTo > :calcDate))' + ) + ->leftJoin(Address::class, 'acp_address', Join::WITH, 'COALESCE(IDENTITY(locationHistory.addressLocation), IDENTITY(personAddress.address)) = acp_address.id'); + + $this->addressHelper->addSelectClauses(ExportAddressHelper::F_ALL, $qb, 'acp_address', 'address_fields'); + + // requestor + $qb + ->addSelect('CASE WHEN acp.requestorPerson IS NULL THEN 1 ELSE 0 END AS isRequestorPerson') + ->addSelect('CASE WHEN acp.requestorPerson IS NULL THEN 0 ELSE 1 END AS isRequestorThirdParty') + ->addSelect('IDENTITY(acp.requestorPerson) AS requestorPersonId') + ->addSelect('IDENTITY(acp.requestorThirdParty) AS requestorThirdPartyId') + ->addSelect('IDENTITY(acp.requestorPerson) AS requestorPerson') + ->addSelect('IDENTITY(acp.requestorThirdParty) AS requestorThirdParty'); + + $qb + // scopes + ->addSelect('(SELECT AGGREGATE(scope.name) FROM ' . Scope::class . ' scope WHERE scope MEMBER OF acp.scopes) AS scopes') + // social issues + ->addSelect('(SELECT AGGREGATE(socialIssue.id) FROM ' . SocialIssue::class . ' socialIssue WHERE socialIssue MEMBER OF acp.socialIssues) AS socialIssues'); + + // add parameter + $qb->setParameter('calcDate', $calcDate); + } +} diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 2f7800e2b..08e51aeab 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -1175,7 +1175,7 @@ export: referrerSince: Référent depuis le locationIsPerson: Parcours localisé auprès d'un usager concerné locationIsTemp: Parcours avec une localisation temporaire - acpLocationPersonName: Usager auprès duquel le parcours est localisé + locationPersonName: Usager auprès duquel le parcours est localisé locationPersonId: Identifiant de l'usager auprès duquel le parcours est localisé acpaddress_fieldscountry: Pays de l'adresse isRequestorPerson: Le demandeur est-il un usager ? From 56d9072abe971cf69f07a55a931e8becabe8fb68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 7 Jul 2023 09:33:03 +0200 Subject: [PATCH 26/50] change id, to avoid collision between ListPersonHelper and ListAccompanyingPeriodHelper --- .../Export/Helper/ListAccompanyingPeriodHelper.php | 6 ++++-- .../ChillPersonBundle/Export/Helper/ListPersonHelper.php | 7 ++++++- src/Bundle/ChillPersonBundle/translations/messages.fr.yml | 4 +++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php b/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php index 8671b4977..a32d78904 100644 --- a/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php +++ b/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php @@ -32,7 +32,7 @@ use Symfony\Contracts\Translation\TranslatorInterface; final readonly class ListAccompanyingPeriodHelper { public const FIELDS = [ - 'id', + 'acpId', 'step', 'stepSince', 'openingDate', @@ -219,8 +219,10 @@ final readonly class ListAccompanyingPeriodHelper public function addSelectClauses(QueryBuilder $qb, \DateTimeImmutable $calcDate): void { + $qb->addSelect('acp.id AS acpId'); + // add the regular fields - foreach (['id', 'openingDate', 'closingDate', 'confidential', 'emergency', 'intensity', 'createdAt', 'updatedAt'] as $field) { + foreach (['openingDate', 'closingDate', 'confidential', 'emergency', 'intensity', 'createdAt', 'updatedAt'] as $field) { $qb->addSelect(sprintf('acp.%s AS %s', $field, $field)); } diff --git a/src/Bundle/ChillPersonBundle/Export/Helper/ListPersonHelper.php b/src/Bundle/ChillPersonBundle/Export/Helper/ListPersonHelper.php index 77a1d9c86..697019ca3 100644 --- a/src/Bundle/ChillPersonBundle/Export/Helper/ListPersonHelper.php +++ b/src/Bundle/ChillPersonBundle/Export/Helper/ListPersonHelper.php @@ -42,7 +42,7 @@ use function strlen; class ListPersonHelper { public const FIELDS = [ - 'id', + 'personId', 'civility', 'firstName', 'lastName', @@ -124,6 +124,11 @@ class ListPersonHelper } switch ($f) { + case 'personId': + $qb->addSelect('person.id AS personId'); + + break; + case 'countryOfBirth': case 'nationality': $qb->addSelect(sprintf('IDENTITY(person.%s) as %s', $f, $f)); diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 08e51aeab..0a82fc330 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -997,6 +997,8 @@ notification: Notify referrer: Notifier le référent Notify any: Notifier d'autres utilisateurs +personId: Identifiant de l'usager + export: export: acp_stats: @@ -1152,7 +1154,7 @@ export: Generate a list of accompanying periods, filtered on different parameters.: Génère une liste des parcours d'accompagnement, filtrée sur différents paramètres. Date of calculation for associated elements: Date de calcul des éléments associés The associated referree, localisation, and other elements will be valid at this date: Les éléments associés, comme la localisation, le référent et d'autres éléments seront valides à cette date - id: Identifiant du parcours + acpId: Identifiant du parcours openingDate: Date d'ouverture du parcours closingDate: Date de fermeture du parcours closingMotive: Motif de cloture From 7f30742fc3615311500931f9d22563f228e3c752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 7 Jul 2023 09:36:39 +0200 Subject: [PATCH 27/50] Rename ListPersonWithAccompanyingPeriod to ListPersonHavingAccompanyingPeriod --- ...ngPeriod.php => ListPersonHavingAccompanyingPeriod.php} | 7 ++++++- .../ChillPersonBundle/config/services/exports_person.yaml | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) rename src/Bundle/ChillPersonBundle/Export/Export/{ListPersonWithAccompanyingPeriod.php => ListPersonHavingAccompanyingPeriod.php} (96%) diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonHavingAccompanyingPeriod.php similarity index 96% rename from src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriod.php rename to src/Bundle/ChillPersonBundle/Export/Export/ListPersonHavingAccompanyingPeriod.php index 370046232..f2e4de4e3 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonHavingAccompanyingPeriod.php @@ -35,7 +35,12 @@ use function count; use function in_array; use function strlen; -class ListPersonWithAccompanyingPeriod implements ExportElementValidatedInterface, ListInterface, GroupedExportInterface +/** + * List the persons, having an accompanying period. + * + * Details of the accompanying period are not included + */ +class ListPersonHavingAccompanyingPeriod implements ExportElementValidatedInterface, ListInterface, GroupedExportInterface { private ExportAddressHelper $addressHelper; diff --git a/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml b/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml index 64360aee9..81875fafb 100644 --- a/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml @@ -24,7 +24,7 @@ services: tags: - { name: chill.export, alias: list_person } - Chill\PersonBundle\Export\Export\ListPersonWithAccompanyingPeriod: + Chill\PersonBundle\Export\Export\ListPersonHavingAccompanyingPeriod: autowire: true autoconfigure: true tags: From 17d2b795b41d9aee8457ad9ade199e07d1c5be60 Mon Sep 17 00:00:00 2001 From: Mathieu Jaumotte Date: Fri, 7 Jul 2023 11:38:00 +0200 Subject: [PATCH 28/50] update changelog --- .changes/unreleased/DX-20230623-122408.yaml | 5 --- .../unreleased/Feature-20230623-122530.yaml | 5 --- .../unreleased/Feature-20230623-122702.yaml | 6 --- .../unreleased/Feature-20230623-124438.yaml | 5 --- .../unreleased/Fixed-20230628-170055.yaml | 6 --- .../unreleased/Fixed-20230629-124412.yaml | 6 --- .../unreleased/Fixed-20230629-231503.yaml | 5 --- .../unreleased/Fixed-20230630-171119.yaml | 5 --- .../unreleased/Fixed-20230630-171153.yaml | 5 --- .changie.yaml | 2 + CHANGELOG.md | 37 +++++++++++++++++++ .../Resources/public/chill/scss/forms.scss | 2 +- 12 files changed, 40 insertions(+), 49 deletions(-) delete mode 100644 .changes/unreleased/DX-20230623-122408.yaml delete mode 100644 .changes/unreleased/Feature-20230623-122530.yaml delete mode 100644 .changes/unreleased/Feature-20230623-122702.yaml delete mode 100644 .changes/unreleased/Feature-20230623-124438.yaml delete mode 100644 .changes/unreleased/Fixed-20230628-170055.yaml delete mode 100644 .changes/unreleased/Fixed-20230629-124412.yaml delete mode 100644 .changes/unreleased/Fixed-20230629-231503.yaml delete mode 100644 .changes/unreleased/Fixed-20230630-171119.yaml delete mode 100644 .changes/unreleased/Fixed-20230630-171153.yaml diff --git a/.changes/unreleased/DX-20230623-122408.yaml b/.changes/unreleased/DX-20230623-122408.yaml deleted file mode 100644 index 58dd96180..000000000 --- a/.changes/unreleased/DX-20230623-122408.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: DX -body: '[FilterOrderHelper] add entity choice and singleCheckbox' -time: 2023-06-23T12:24:08.133491895+02:00 -custom: - Issue: "" diff --git a/.changes/unreleased/Feature-20230623-122530.yaml b/.changes/unreleased/Feature-20230623-122530.yaml deleted file mode 100644 index 922750ea8..000000000 --- a/.changes/unreleased/Feature-20230623-122530.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Feature -body: '[activity list] add filtering for activities list' -time: 2023-06-23T12:25:30.49643551+02:00 -custom: - Issue: "" diff --git a/.changes/unreleased/Feature-20230623-122702.yaml b/.changes/unreleased/Feature-20230623-122702.yaml deleted file mode 100644 index e1d1b0e1f..000000000 --- a/.changes/unreleased/Feature-20230623-122702.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: Feature -body: '[activity list] in person context, show also the activities from the accompanying - periods where the person participates' -time: 2023-06-23T12:27:02.159041095+02:00 -custom: - Issue: "" diff --git a/.changes/unreleased/Feature-20230623-124438.yaml b/.changes/unreleased/Feature-20230623-124438.yaml deleted file mode 100644 index bc199d3bb..000000000 --- a/.changes/unreleased/Feature-20230623-124438.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Feature -body: '[activity list] add pagination to the list of activities' -time: 2023-06-23T12:44:38.879098862+02:00 -custom: - Issue: "" diff --git a/.changes/unreleased/Fixed-20230628-170055.yaml b/.changes/unreleased/Fixed-20230628-170055.yaml deleted file mode 100644 index 7f9ec3028..000000000 --- a/.changes/unreleased/Fixed-20230628-170055.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: Fixed -body: '[export] Rename label for CurrentActionFilter (on accompanying period work) - to make precision between "ouvert" and "sans date de fin"' -time: 2023-06-28T17:00:55.206937751+02:00 -custom: - Issue: "" diff --git a/.changes/unreleased/Fixed-20230629-124412.yaml b/.changes/unreleased/Fixed-20230629-124412.yaml deleted file mode 100644 index 7fc3d3eb0..000000000 --- a/.changes/unreleased/Fixed-20230629-124412.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: Fixed -body: Force the db to have either a person_location or a address_location, and avoid - to have both also internally in the entity -time: 2023-06-29T12:44:12.019663991+02:00 -custom: - Issue: "" diff --git a/.changes/unreleased/Fixed-20230629-231503.yaml b/.changes/unreleased/Fixed-20230629-231503.yaml deleted file mode 100644 index e021d1fda..000000000 --- a/.changes/unreleased/Fixed-20230629-231503.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Fixed -body: '[export] set rolling date on person age aggregator' -time: 2023-06-29T23:15:03.20841309+02:00 -custom: - Issue: "" diff --git a/.changes/unreleased/Fixed-20230630-171119.yaml b/.changes/unreleased/Fixed-20230630-171119.yaml deleted file mode 100644 index f3185ace2..000000000 --- a/.changes/unreleased/Fixed-20230630-171119.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Fixed -body: '[export] fix list when a person locating a course is without address' -time: 2023-06-30T17:11:19.454081914+02:00 -custom: - Issue: "" diff --git a/.changes/unreleased/Fixed-20230630-171153.yaml b/.changes/unreleased/Fixed-20230630-171153.yaml deleted file mode 100644 index c09bd93d0..000000000 --- a/.changes/unreleased/Fixed-20230630-171153.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Fixed -body: '[export] remove unused condition on course about duration participation' -time: 2023-06-30T17:11:53.076615549+02:00 -custom: - Issue: "" diff --git a/.changie.yaml b/.changie.yaml index 8a25ed695..cda69de65 100644 --- a/.changie.yaml +++ b/.changie.yaml @@ -30,6 +30,8 @@ kinds: auto: patch - label: DX auto: patch + - label: UX + auto: patch newlines: afterChangelogHeader: 1 beforeChangelogVersion: 1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 93ff93556..1f51bf06e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,43 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). +## v2.4.0 - 2023-07-07 +### Feature +* [activity list] add filtering for activities list + + +* [activity list] in person context, show also the activities from the accompanying periods where the person participates + + +* [activity list] add pagination to the list of activities + + +* ([#118](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/118)) improve UX design for filterOrder box + + + + +### Fixed +* [export] Rename label for CurrentActionFilter (on accompanying period work) to make precision between "ouvert" and "sans date de fin" + + +* Force the db to have either a person_location or a address_location, and avoid to have both also internally in the entity + + +* [export] set rolling date on person age aggregator + + +* [export] fix list when a person locating a course is without address + + +* [export] remove unused condition on course about duration participation + + +### DX +* [FilterOrderHelper] add entity choice and singleCheckbox + + + ## v2.3.0 - 2023-06-27 ### Feature * ([#110](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/110)) Edit saved exports options: the saved exports options (forms, filters, aggregators) are now editable. diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/forms.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/forms.scss index a517a5516..28c597bc0 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/forms.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/forms.scss @@ -44,5 +44,5 @@ form { } .chill_filter_order { - background: $gray-100; + background: $gray-100; } \ No newline at end of file From c8146ded17b332a3566b58b7c824840259b7f7ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 7 Jul 2023 12:36:24 +0200 Subject: [PATCH 29/50] Feature: add a list for people with their associated accompanying course --- .../unreleased/Feature-20230707-123609.yaml | 5 + .../Export/ExportInterface.php | 2 +- ...istPersonWithAccompanyingPeriodDetails.php | 149 ++++++++++++++++++ .../Helper/ListAccompanyingPeriodHelper.php | 39 +++-- .../Export/Helper/ListPersonHelper.php | 42 ++--- .../config/services/exports_person.yaml | 20 +-- .../translations/messages.fr.yml | 10 +- 7 files changed, 213 insertions(+), 54 deletions(-) create mode 100644 .changes/unreleased/Feature-20230707-123609.yaml create mode 100644 src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriodDetails.php diff --git a/.changes/unreleased/Feature-20230707-123609.yaml b/.changes/unreleased/Feature-20230707-123609.yaml new file mode 100644 index 000000000..51ff94d4c --- /dev/null +++ b/.changes/unreleased/Feature-20230707-123609.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: '[export] Add a list for people with their associated course' +time: 2023-07-07T12:36:09.596469063+02:00 +custom: + Issue: "125" diff --git a/src/Bundle/ChillMainBundle/Export/ExportInterface.php b/src/Bundle/ChillMainBundle/Export/ExportInterface.php index f357a9fdb..a11a51746 100644 --- a/src/Bundle/ChillMainBundle/Export/ExportInterface.php +++ b/src/Bundle/ChillMainBundle/Export/ExportInterface.php @@ -97,7 +97,7 @@ interface ExportInterface extends ExportElementInterface * @param mixed[] $values The values from the result. if there are duplicates, those might be given twice. Example: array('FR', 'BE', 'CZ', 'FR', 'BE', 'FR') * @param mixed $data The data from the export's form (as defined in `buildForm`) * - * @return callable(null|string|int|float|'_header' $value): string|int|\DateTimeInterface where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }` + * @return (callable(null|string|int|float|'_header' $value): string|int|\DateTimeInterface) where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }` */ public function getLabels($key, array $values, $data); diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriodDetails.php b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriodDetails.php new file mode 100644 index 000000000..295d87842 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriodDetails.php @@ -0,0 +1,149 @@ +add('address_date', PickRollingDateType::class, [ + 'label' => 'Data valid at this date', + 'help' => 'Data regarding center, addresses, and so on will be computed at this date', + ]); + } + public function getFormDefaultData(): array + { + return ['address_date' => new RollingDate(RollingDate::T_TODAY)]; + } + + public function getAllowedFormattersTypes() + { + return [FormatterInterface::TYPE_LIST]; + } + + public function getDescription() + { + return 'export.list.person_with_acp.Create a list of people having an accompaying periods with details of period, according to various filters.'; + } + + public function getGroup(): string + { + return 'Exports of persons'; + } + + public function getLabels($key, array $values, $data) + { + if (in_array($key, $this->listPersonHelper->getAllKeys(), true)) { + return $this->listPersonHelper->getLabels($key, $values, $data); + } + + return $this->listAccompanyingPeriodHelper->getLabels($key, $values, $data); + } + + public function getQueryKeys($data) + { + return array_merge( + $this->listPersonHelper->getAllKeys(), + $this->listAccompanyingPeriodHelper->getQueryKeys($data), + ); + } + + public function getResult($query, $data) + { + return $query->getQuery()->getResult(AbstractQuery::HYDRATE_SCALAR); + } + + public function getTitle() + { + return 'export.list.person_with_acp.List peoples having an accompanying period with period details'; + } + + public function getType() + { + return Declarations::PERSON_TYPE; + } + + /** + * param array{fields: string[], address_date: DateTimeImmutable} $data. + */ + public function initiateQuery(array $requiredModifiers, array $acl, array $data = []) + { + $centers = array_map(static fn ($el) => $el['center'], $acl); + + $qb = $this->entityManager->createQueryBuilder(); + + $qb->from(Person::class, 'person') + ->join('person.accompanyingPeriodParticipations', 'acppart') + ->join('acppart.accompanyingPeriod', 'acp') + ->andWhere($qb->expr()->neq('acp.step', "'" . AccompanyingPeriod::STEP_DRAFT . "'")) + ->andWhere( + $qb->expr()->exists( + 'SELECT 1 FROM ' . PersonCenterHistory::class . ' pch WHERE pch.person = person.id AND pch.center IN (:authorized_centers)' + ) + )->setParameter('authorized_centers', $centers); + + $this->listPersonHelper->addSelect($qb, ListPersonHelper::FIELDS, $this->rollingDateConverter->convert($data['address_date'])); + $this->listAccompanyingPeriodHelper->addSelectClauses($qb, $this->rollingDateConverter->convert($data['address_date'])); + + return $qb; + } + + public function requiredRole(): string + { + return PersonVoter::LISTS; + } + + public function supportsModifiers() + { + return [Declarations::PERSON_TYPE, Declarations::PERSON_IMPLIED_IN, Declarations::ACP_TYPE]; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php b/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php index a32d78904..5fa2252cd 100644 --- a/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php +++ b/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php @@ -58,10 +58,10 @@ final readonly class ListAccompanyingPeriodHelper 'requestorThirdPartyId', 'scopes', 'socialIssues', - 'createdAt', - 'createdBy', - 'updatedAt', - 'updatedBy', + 'acpCreatedAt', + 'acpCreatedBy', + 'acpUpdatedAt', + 'acpUpdatedBy', ]; public function __construct( @@ -82,14 +82,14 @@ final readonly class ListAccompanyingPeriodHelper { return array_merge( ListAccompanyingPeriodHelper::FIELDS, - $this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields') + $this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'acp_address_fields') ); } public function getLabels($key, array $values, $data) { - if (substr($key, 0, strlen('address_fields')) === 'address_fields') { - return $this->addressHelper->getLabel($key, $values, $data, 'address_fields'); + if (str_starts_with($key, 'acp_address_fields')) { + return $this->addressHelper->getLabel($key, $values, $data, 'acp_address_fields'); } switch ($key) { @@ -97,8 +97,8 @@ final readonly class ListAccompanyingPeriodHelper case 'openingDate': case 'closingDate': case 'referrerSince': - case 'createdAt': - case 'updatedAt': + case 'acpCreatedAt': + case 'acpUpdatedAt': return $this->dateTimeHelper->getLabel('export.list.acp.' . $key); case 'origin': @@ -220,14 +220,23 @@ final readonly class ListAccompanyingPeriodHelper public function addSelectClauses(QueryBuilder $qb, \DateTimeImmutable $calcDate): void { $qb->addSelect('acp.id AS acpId'); + $qb->addSelect('acp.createdAt AS acpCreatedAt'); + $qb->addSelect('acp.updatedAt AS acpUpdatedAt'); // add the regular fields - foreach (['openingDate', 'closingDate', 'confidential', 'emergency', 'intensity', 'createdAt', 'updatedAt'] as $field) { + foreach (['openingDate', 'closingDate', 'confidential', 'emergency', 'intensity'] as $field) { $qb->addSelect(sprintf('acp.%s AS %s', $field, $field)); } // add the field which are simple association - foreach (['origin' => 'label', 'closingMotive' => 'name', 'job' => 'label', 'createdBy' => 'label', 'updatedBy' => 'label', 'administrativeLocation' => 'name'] as $entity => $field) { + $qb + ->leftJoin('acp.createdBy', "acp_created_by_t") + ->addSelect('acp_created_by_t.label AS acpCreatedBy'); + $qb + ->leftJoin('acp.updatedBy', "acp_updated_by_t") + ->addSelect('acp_updated_by_t.label AS acpUpdatedBy'); + + foreach (['origin' => 'label', 'closingMotive' => 'name', 'job' => 'label', 'administrativeLocation' => 'name'] as $entity => $field) { $qb ->leftJoin(sprintf('acp.%s', $entity), "{$entity}_t") ->addSelect(sprintf('%s_t.%s AS %s', $entity, $field, $entity)); @@ -279,13 +288,13 @@ final readonly class ListAccompanyingPeriodHelper ) ->leftJoin( PersonHouseholdAddress::class, - 'personAddress', + 'acpPersonAddress', Join::WITH, - 'locationHistory.personLocation = personAddress.person AND (personAddress.validFrom <= :calcDate AND (personAddress.validTo IS NULL OR personAddress.validTo > :calcDate))' + 'locationHistory.personLocation = acpPersonAddress.person AND (acpPersonAddress.validFrom <= :calcDate AND (acpPersonAddress.validTo IS NULL OR acpPersonAddress.validTo > :calcDate))' ) - ->leftJoin(Address::class, 'acp_address', Join::WITH, 'COALESCE(IDENTITY(locationHistory.addressLocation), IDENTITY(personAddress.address)) = acp_address.id'); + ->leftJoin(Address::class, 'acp_address', Join::WITH, 'COALESCE(IDENTITY(locationHistory.addressLocation), IDENTITY(acpPersonAddress.address)) = acp_address.id'); - $this->addressHelper->addSelectClauses(ExportAddressHelper::F_ALL, $qb, 'acp_address', 'address_fields'); + $this->addressHelper->addSelectClauses(ExportAddressHelper::F_ALL, $qb, 'acp_address', 'acp_address_fields'); // requestor $qb diff --git a/src/Bundle/ChillPersonBundle/Export/Helper/ListPersonHelper.php b/src/Bundle/ChillPersonBundle/Export/Helper/ListPersonHelper.php index 697019ca3..198794326 100644 --- a/src/Bundle/ChillPersonBundle/Export/Helper/ListPersonHelper.php +++ b/src/Bundle/ChillPersonBundle/Export/Helper/ListPersonHelper.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\PersonBundle\Export\Helper; +use Chill\MainBundle\Entity\Language; use Chill\MainBundle\Export\Helper\ExportAddressHelper; use Chill\MainBundle\Repository\CenterRepositoryInterface; use Chill\MainBundle\Repository\CivilityRepositoryInterface; @@ -114,7 +115,26 @@ class ListPersonHelper } /** - * @param array|value-of[] $fields + * Those keys are the "direct" keys, which are created when we decide to use to list all the keys. + * + * This method must be used in `getKeys` instead of the `self::FIELDS` + * + * @return array + */ + public function getAllKeys(): array + { + return [ + ...array_filter( + ListPersonHelper::FIELDS, + fn (string $key) => !in_array($key, ['address_fields', 'lifecycleUpdate'], true) + ), + ...$this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields'), + ...['createdAt', 'createdBy', 'updatedAt', 'updatedBy'], + ]; + } + + /** + * @param array> $fields */ public function addSelect(QueryBuilder $qb, array $fields, DateTimeImmutable $computedDate): void { @@ -143,25 +163,7 @@ class ListPersonHelper break; case 'spokenLanguages': - $qb - ->leftJoin('person.spokenLanguages', 'spokenLanguage') - ->addSelect('AGGREGATE(spokenLanguage.id) AS spokenLanguages') - ->addGroupBy('person'); - - if (in_array('center', $fields, true)) { - $qb->addGroupBy('center'); - } - - if (in_array('address_fields', $fields, true)) { - $qb - ->addGroupBy('address_fieldsid') - ->addGroupBy('address_fieldscountry_t.id') - ->addGroupBy('address_fieldspostcode_t.id'); - } - - if (in_array('household_id', $fields, true)) { - $qb->addGroupBy('household_id'); - } + $qb->addSelect('(SELECT AGGREGATE(language.id) FROM ' . Language::class . ' language WHERE language MEMBER OF person.spokenLanguages) AS spokenLanguages'); break; diff --git a/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml b/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml index 81875fafb..43f5556cf 100644 --- a/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml @@ -4,35 +4,27 @@ services: autowire: true ## Indicators - chill.person.export.count_person: - class: Chill\PersonBundle\Export\Export\CountPerson - autowire: true - autoconfigure: true + Chill\PersonBundle\Export\Export\CountPerson: tags: - { name: chill.export, alias: count_person } - chill.person.export.count_person_with_accompanying_course: - class: Chill\PersonBundle\Export\Export\CountPersonWithAccompanyingCourse - autowire: true - autoconfigure: true + Chill\PersonBundle\Export\Export\CountPersonWithAccompanyingCourse: tags: - { name: chill.export, alias: count_person_with_accompanying_course } Chill\PersonBundle\Export\Export\ListPerson: - autowire: true - autoconfigure: true tags: - { name: chill.export, alias: list_person } Chill\PersonBundle\Export\Export\ListPersonHavingAccompanyingPeriod: - autowire: true - autoconfigure: true tags: - { name: chill.export, alias: list_person_with_acp } + Chill\PersonBundle\Export\Export\ListPersonWithAccompanyingPeriodDetails: + tags: + - { name: chill.export, alias: list_person_with_acp_details } + Chill\PersonBundle\Export\Export\ListAccompanyingPeriod: - autowire: true - autoconfigure: true tags: - { name: chill.export, alias: list_acp } diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 0a82fc330..1159b2c49 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -1148,7 +1148,9 @@ export: list: person_with_acp: List peoples having an accompanying period: Liste des usagers ayant un parcours d'accompagnement + List peoples having an accompanying period with period details: Liste des usagers concernés avec détail de chaque parcours Create a list of people having an accompaying periods, according to various filters.: Génère une liste des usagers ayant un parcours d'accompagnement, selon différents critères liés au parcours ou à l'usager + Create a list of people having an accompaying periods with details of period, according to various filters.: Génère une liste des usagers ayant un parcours d'accompagnement, selon différents critères liés au parcours ou à l'usager. Ajoute les détails du parcours à la liste. acp: List of accompanying periods: Liste des parcours d'accompagnements Generate a list of accompanying periods, filtered on different parameters.: Génère une liste des parcours d'accompagnement, filtrée sur différents paramètres. @@ -1162,14 +1164,14 @@ export: confidential: Confidentiel emergency: Urgent intensity: Intensité - createdAt: Créé le - updatedAt: Dernière mise à jour le + acpCreatedAt: Créé le + acpUpdatedAt: Dernière mise à jour le acpOrigin: Origine du parcours origin: Origine du parcours acpClosingMotive: Motif de fermeture acpJob: Métier du parcours - createdBy: Créé par - updatedBy: Dernière modification par + acpCreatedBy: Créé par + acpUpdatedBy: Dernière modification par administrativeLocation: Location administrative step: Etape stepSince: Dernière modification de l'étape From 63f9bd554897e1363d0b321c9e80c28e2b7745bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 7 Jul 2023 12:42:32 +0200 Subject: [PATCH 30/50] [export] Add ordering by person''s lastname or course opening date in list which concerns accompanying course or people --- .changes/unreleased/Feature-20230707-124132.yaml | 6 ++++++ .../Export/Export/ListAccompanyingPeriod.php | 5 +++++ .../Export/Export/ListPersonHavingAccompanyingPeriod.php | 5 +++++ .../Export/ListPersonWithAccompanyingPeriodDetails.php | 6 ++++++ 4 files changed, 22 insertions(+) create mode 100644 .changes/unreleased/Feature-20230707-124132.yaml diff --git a/.changes/unreleased/Feature-20230707-124132.yaml b/.changes/unreleased/Feature-20230707-124132.yaml new file mode 100644 index 000000000..4ad93ad22 --- /dev/null +++ b/.changes/unreleased/Feature-20230707-124132.yaml @@ -0,0 +1,6 @@ +kind: Feature +body: '[export] Add ordering by person''s lastname or course opening date in list + which concerns accompanying course or peoples' +time: 2023-07-07T12:41:32.112725962+02:00 +custom: + Issue: "" diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriod.php index 3f7821bbc..ab9c0db2f 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListAccompanyingPeriod.php @@ -133,6 +133,11 @@ final readonly class ListAccompanyingPeriod implements ListInterface, GroupedExp $this->listAccompanyingPeriodHelper->addSelectClauses($qb, $this->rollingDateConverter->convert($data['calc_date'])); + $qb + ->addOrderBy('acp.openingDate') + ->addOrderBy('acp.closingDate') + ->addOrderBy('acp.id'); + return $qb; } diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonHavingAccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonHavingAccompanyingPeriod.php index f2e4de4e3..408d0b3af 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonHavingAccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonHavingAccompanyingPeriod.php @@ -190,6 +190,11 @@ class ListPersonHavingAccompanyingPeriod implements ExportElementValidatedInterf $this->listPersonHelper->addSelect($qb, $fields, $data['address_date']); + $qb + ->addOrderBy('person.lastName') + ->addOrderBy('person.firstName') + ->addOrderBy('person.id'); + return $qb; } diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriodDetails.php b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriodDetails.php index 295d87842..ddb16bb2d 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriodDetails.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriodDetails.php @@ -134,6 +134,12 @@ final readonly class ListPersonWithAccompanyingPeriodDetails implements ListInte $this->listPersonHelper->addSelect($qb, ListPersonHelper::FIELDS, $this->rollingDateConverter->convert($data['address_date'])); $this->listAccompanyingPeriodHelper->addSelectClauses($qb, $this->rollingDateConverter->convert($data['address_date'])); + $qb + ->addOrderBy('person.lastName') + ->addOrderBy('person.firstName') + ->addOrderBy('person.id') + ->addOrderBy('acp.id'); + return $qb; } From 197d69ef4a09fffd359305b8e1ad0a6f03f466b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 7 Jul 2023 13:21:22 +0200 Subject: [PATCH 31/50] release v2.4.0 --- .changes/unreleased/DX-20230629-160029.yaml | 5 --- .../unreleased/Feature-20230629-131558.yaml | 6 --- .../unreleased/Feature-20230629-173445.yaml | 5 --- .../unreleased/Feature-20230629-173509.yaml | 5 --- .../unreleased/Feature-20230629-173544.yaml | 5 --- .../unreleased/Feature-20230629-173617.yaml | 5 --- .../unreleased/Feature-20230629-173822.yaml | 5 --- .../unreleased/Feature-20230629-173844.yaml | 5 --- .../unreleased/Feature-20230705-220544.yaml | 6 --- .../unreleased/Feature-20230706-213428.yaml | 5 --- .../unreleased/Fixed-20230627-110233.yaml | 6 --- .../unreleased/Fixed-20230628-170055.yaml | 6 --- .../unreleased/Fixed-20230629-124412.yaml | 6 --- .../unreleased/Fixed-20230629-231503.yaml | 5 --- .../unreleased/Fixed-20230630-171119.yaml | 5 --- .../unreleased/Fixed-20230630-171153.yaml | 5 --- .../unreleased/Fixed-20230706-220125.yaml | 6 --- .changes/v2.4.0.md | 36 ++++++++++++++++++ CHANGELOG.md | 37 +++++++++++++++++++ 19 files changed, 73 insertions(+), 91 deletions(-) delete mode 100644 .changes/unreleased/DX-20230629-160029.yaml delete mode 100644 .changes/unreleased/Feature-20230629-131558.yaml delete mode 100644 .changes/unreleased/Feature-20230629-173445.yaml delete mode 100644 .changes/unreleased/Feature-20230629-173509.yaml delete mode 100644 .changes/unreleased/Feature-20230629-173544.yaml delete mode 100644 .changes/unreleased/Feature-20230629-173617.yaml delete mode 100644 .changes/unreleased/Feature-20230629-173822.yaml delete mode 100644 .changes/unreleased/Feature-20230629-173844.yaml delete mode 100644 .changes/unreleased/Feature-20230705-220544.yaml delete mode 100644 .changes/unreleased/Feature-20230706-213428.yaml delete mode 100644 .changes/unreleased/Fixed-20230627-110233.yaml delete mode 100644 .changes/unreleased/Fixed-20230628-170055.yaml delete mode 100644 .changes/unreleased/Fixed-20230629-124412.yaml delete mode 100644 .changes/unreleased/Fixed-20230629-231503.yaml delete mode 100644 .changes/unreleased/Fixed-20230630-171119.yaml delete mode 100644 .changes/unreleased/Fixed-20230630-171153.yaml delete mode 100644 .changes/unreleased/Fixed-20230706-220125.yaml create mode 100644 .changes/v2.4.0.md diff --git a/.changes/unreleased/DX-20230629-160029.yaml b/.changes/unreleased/DX-20230629-160029.yaml deleted file mode 100644 index b83befec6..000000000 --- a/.changes/unreleased/DX-20230629-160029.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: DX -body: 'Rolling Date: can receive a null parameter' -time: 2023-06-29T16:00:29.664814895+02:00 -custom: - Issue: "" diff --git a/.changes/unreleased/Feature-20230629-131558.yaml b/.changes/unreleased/Feature-20230629-131558.yaml deleted file mode 100644 index 42d731c4f..000000000 --- a/.changes/unreleased/Feature-20230629-131558.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: Feature -body: '[export] on "filter by user working" on accompanying period, add two dates - to filters intervention within a period' -time: 2023-06-29T13:15:58.070316708+02:00 -custom: - Issue: "113" diff --git a/.changes/unreleased/Feature-20230629-173445.yaml b/.changes/unreleased/Feature-20230629-173445.yaml deleted file mode 100644 index 2d3642a85..000000000 --- a/.changes/unreleased/Feature-20230629-173445.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Feature -body: '[export] Add an aggregator by user''s job working on a course' -time: 2023-06-29T17:34:45.278993433+02:00 -custom: - Issue: "113" diff --git a/.changes/unreleased/Feature-20230629-173509.yaml b/.changes/unreleased/Feature-20230629-173509.yaml deleted file mode 100644 index 95fd23458..000000000 --- a/.changes/unreleased/Feature-20230629-173509.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Feature -body: '[export] add an aggregator by user''s scope working on a course' -time: 2023-06-29T17:35:09.548758741+02:00 -custom: - Issue: "113" diff --git a/.changes/unreleased/Feature-20230629-173544.yaml b/.changes/unreleased/Feature-20230629-173544.yaml deleted file mode 100644 index 94e79bc6d..000000000 --- a/.changes/unreleased/Feature-20230629-173544.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Feature -body: '[export] on aggregator "user working on a course"' -time: 2023-06-29T17:35:44.998468724+02:00 -custom: - Issue: "" diff --git a/.changes/unreleased/Feature-20230629-173617.yaml b/.changes/unreleased/Feature-20230629-173617.yaml deleted file mode 100644 index 445b85cff..000000000 --- a/.changes/unreleased/Feature-20230629-173617.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Feature -body: '[export] add a center aggregator for Person' -time: 2023-06-29T17:36:17.635876613+02:00 -custom: - Issue: "113" diff --git a/.changes/unreleased/Feature-20230629-173822.yaml b/.changes/unreleased/Feature-20230629-173822.yaml deleted file mode 100644 index 6a1569d00..000000000 --- a/.changes/unreleased/Feature-20230629-173822.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Feature -body: '[export] add a filter on "job working on a course"' -time: 2023-06-29T17:38:22.682951416+02:00 -custom: - Issue: "113" diff --git a/.changes/unreleased/Feature-20230629-173844.yaml b/.changes/unreleased/Feature-20230629-173844.yaml deleted file mode 100644 index f307ebf57..000000000 --- a/.changes/unreleased/Feature-20230629-173844.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Feature -body: '[export] Add a filter on "scope working on a course"' -time: 2023-06-29T17:38:44.238287822+02:00 -custom: - Issue: "113" diff --git a/.changes/unreleased/Feature-20230705-220544.yaml b/.changes/unreleased/Feature-20230705-220544.yaml deleted file mode 100644 index 4212f7646..000000000 --- a/.changes/unreleased/Feature-20230705-220544.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: Feature -body: Create a role "See Confidential Periods", separated from the "Reassign courses" - role -time: 2023-07-05T22:05:44.435112463+02:00 -custom: - Issue: "121" diff --git a/.changes/unreleased/Feature-20230706-213428.yaml b/.changes/unreleased/Feature-20230706-213428.yaml deleted file mode 100644 index 092e5a2e0..000000000 --- a/.changes/unreleased/Feature-20230706-213428.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Feature -body: 'Sync user absence / presence through microsoft outlook / graph api. ' -time: 2023-07-06T21:34:28.973144334+02:00 -custom: - Issue: "124" diff --git a/.changes/unreleased/Fixed-20230627-110233.yaml b/.changes/unreleased/Fixed-20230627-110233.yaml deleted file mode 100644 index 58bb23933..000000000 --- a/.changes/unreleased/Fixed-20230627-110233.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: Fixed -body: On the accompanying course page, open the action on view mode if the user does - not have right to update them (i.e. if the accompanying period is closed) -time: 2023-06-27T11:02:33.027807027+02:00 -custom: - Issue: "116" diff --git a/.changes/unreleased/Fixed-20230628-170055.yaml b/.changes/unreleased/Fixed-20230628-170055.yaml deleted file mode 100644 index 7f9ec3028..000000000 --- a/.changes/unreleased/Fixed-20230628-170055.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: Fixed -body: '[export] Rename label for CurrentActionFilter (on accompanying period work) - to make precision between "ouvert" and "sans date de fin"' -time: 2023-06-28T17:00:55.206937751+02:00 -custom: - Issue: "" diff --git a/.changes/unreleased/Fixed-20230629-124412.yaml b/.changes/unreleased/Fixed-20230629-124412.yaml deleted file mode 100644 index 7fc3d3eb0..000000000 --- a/.changes/unreleased/Fixed-20230629-124412.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: Fixed -body: Force the db to have either a person_location or a address_location, and avoid - to have both also internally in the entity -time: 2023-06-29T12:44:12.019663991+02:00 -custom: - Issue: "" diff --git a/.changes/unreleased/Fixed-20230629-231503.yaml b/.changes/unreleased/Fixed-20230629-231503.yaml deleted file mode 100644 index e021d1fda..000000000 --- a/.changes/unreleased/Fixed-20230629-231503.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Fixed -body: '[export] set rolling date on person age aggregator' -time: 2023-06-29T23:15:03.20841309+02:00 -custom: - Issue: "" diff --git a/.changes/unreleased/Fixed-20230630-171119.yaml b/.changes/unreleased/Fixed-20230630-171119.yaml deleted file mode 100644 index f3185ace2..000000000 --- a/.changes/unreleased/Fixed-20230630-171119.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Fixed -body: '[export] fix list when a person locating a course is without address' -time: 2023-06-30T17:11:19.454081914+02:00 -custom: - Issue: "" diff --git a/.changes/unreleased/Fixed-20230630-171153.yaml b/.changes/unreleased/Fixed-20230630-171153.yaml deleted file mode 100644 index c09bd93d0..000000000 --- a/.changes/unreleased/Fixed-20230630-171153.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Fixed -body: '[export] remove unused condition on course about duration participation' -time: 2023-06-30T17:11:53.076615549+02:00 -custom: - Issue: "" diff --git a/.changes/unreleased/Fixed-20230706-220125.yaml b/.changes/unreleased/Fixed-20230706-220125.yaml deleted file mode 100644 index b9449d514..000000000 --- a/.changes/unreleased/Fixed-20230706-220125.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: Fixed -body: 'Command to subscribe on MS Graph users calendars: improve the loop to be more - efficient' -time: 2023-07-06T22:01:25.847374805+02:00 -custom: - Issue: "" diff --git a/.changes/v2.4.0.md b/.changes/v2.4.0.md new file mode 100644 index 000000000..522957300 --- /dev/null +++ b/.changes/v2.4.0.md @@ -0,0 +1,36 @@ +## v2.4.0 - 2023-07-07 + +### Feature +* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] on "filter by user working" on accompanying period, add two dates to filters intervention within a period +* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] Add an aggregator by user's job working on a course +* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] add an aggregator by user's scope working on a course +* [export] on aggregator "user working on a course" +* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] add a center aggregator for Person +* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] add a filter on "job working on a course" +* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] Add a filter on "scope working on a course" +* ([#121](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/121)) Create a role "See Confidential Periods", separated from the "Reassign courses" role +* ([#124](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/124)) Sync user absence / presence through microsoft outlook / graph api. + +### Fixed +* ([#116](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/116)) On the accompanying course page, open the action on view mode if the user does not have right to update them (i.e. if the accompanying period is closed) +* [export] Rename label for CurrentActionFilter (on accompanying period work) to make precision between "ouvert" and "sans date de fin" +* Force the db to have either a person_location or a address_location, and avoid to have both also internally in the entity +* [export] set rolling date on person age aggregator +* [export] fix list when a person locating a course is without address +* [export] remove unused condition on course about duration participation +* Command to subscribe on MS Graph users calendars: improve the loop to be more efficient + +### DX +* Rolling Date: can receive a null parameter + +### Traduction francophone des principaux changements + +- sur le "filtre par intervenant", ajoute deux dates pour limiter la période d'intervention; +- ajout d'un regroupement par métier des intervenants sur un parcours; +- ajout d'un regroupement par service des intervenants sur un parcours; +- ajout d'un regroupement par utilisateur intervenant sur un parcours +- ajout d'un regroupement "par centre de l'usager"; +- ajout d'un filtre "par métier intervenant sur un parcours"; +- ajout d'un filtre "par service intervenant sur un parcours"; +- création d'un rôle spécifique pour voir les parcours confidentiels (et séparer de celui de la liste qui permet de ré-assigner les parcours en lot); +- synchronisation de l'absence des utilisateurs par microsoft graph api diff --git a/CHANGELOG.md b/CHANGELOG.md index 93ff93556..b74eea58d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,43 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). +## v2.4.0 - 2023-07-07 + +### Feature +* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] on "filter by user working" on accompanying period, add two dates to filters intervention within a period +* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] Add an aggregator by user's job working on a course +* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] add an aggregator by user's scope working on a course +* [export] on aggregator "user working on a course" +* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] add a center aggregator for Person +* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] add a filter on "job working on a course" +* ([#113](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/113)) [export] Add a filter on "scope working on a course" +* ([#121](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/121)) Create a role "See Confidential Periods", separated from the "Reassign courses" role +* ([#124](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/124)) Sync user absence / presence through microsoft outlook / graph api. + +### Fixed +* ([#116](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/116)) On the accompanying course page, open the action on view mode if the user does not have right to update them (i.e. if the accompanying period is closed) +* [export] Rename label for CurrentActionFilter (on accompanying period work) to make precision between "ouvert" and "sans date de fin" +* Force the db to have either a person_location or a address_location, and avoid to have both also internally in the entity +* [export] set rolling date on person age aggregator +* [export] fix list when a person locating a course is without address +* [export] remove unused condition on course about duration participation +* Command to subscribe on MS Graph users calendars: improve the loop to be more efficient + +### DX +* Rolling Date: can receive a null parameter + +### Traduction francophone des principaux changements + +- sur le "filtre par intervenant", ajoute deux dates pour limiter la période d'intervention; +- ajout d'un regroupement par métier des intervenants sur un parcours; +- ajout d'un regroupement par service des intervenants sur un parcours; +- ajout d'un regroupement par utilisateur intervenant sur un parcours +- ajout d'un regroupement "par centre de l'usager"; +- ajout d'un filtre "par métier intervenant sur un parcours"; +- ajout d'un filtre "par service intervenant sur un parcours"; +- création d'un rôle spécifique pour voir les parcours confidentiels (et séparer de celui de la liste qui permet de ré-assigner les parcours en lot); +- synchronisation de l'absence des utilisateurs par microsoft graph api + ## v2.3.0 - 2023-06-27 ### Feature * ([#110](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/110)) Edit saved exports options: the saved exports options (forms, filters, aggregators) are now editable. From 20e64e87680d102c686000f1c35d0139d00eecdf Mon Sep 17 00:00:00 2001 From: Mathieu Jaumotte Date: Fri, 7 Jul 2023 15:41:29 +0200 Subject: [PATCH 32/50] test filterOrder in an accordion --- .../views/FilterOrder/base.html.twig | 162 ++++++++++-------- 1 file changed, 86 insertions(+), 76 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig index b2673b60c..f642c82aa 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig @@ -1,92 +1,102 @@ {{ form_start(form) }} - {% set btnSubmit = 0 %} -
-
- {% if form.vars.has_search_box %} -
-
- {{ form_widget(form.q) }} - -
-
- {% endif %} -
- {% if form.dateRanges is defined %} - {% set btnSubmit = 1 %} - {% if form.dateRanges|length > 0 %} - {% for dateRangeName, _o in form.dateRanges %} -
- {% if form.dateRanges[dateRangeName].vars.label is not same as(false) %} - {{ form_label(form.dateRanges[dateRangeName])}} - {% else %} -
{{ 'activity_filter.By date'|trans }}
- {% endif %} -
-
- {{ 'chill_calendar.From'|trans }} - {{ form_widget(form.dateRanges[dateRangeName]['from']) }} - {{ 'chill_calendar.To'|trans }} - {{ form_widget(form.dateRanges[dateRangeName]['to']) }} -
+
+

+ +

+
+ {% set btnSubmit = 0 %} +
+
+ {% if form.vars.has_search_box %} +
+
+ {{ form_widget(form.q) }} +
- {% endfor %} + {% endif %} +
+ {% if form.dateRanges is defined %} + {% set btnSubmit = 1 %} + {% if form.dateRanges|length > 0 %} + {% for dateRangeName, _o in form.dateRanges %} +
+ {% if form.dateRanges[dateRangeName].vars.label is not same as(false) %} + {{ form_label(form.dateRanges[dateRangeName])}} + {% else %} +
{{ 'activity_filter.By date'|trans }}
+ {% endif %} +
+
+ {{ 'chill_calendar.From'|trans }} + {{ form_widget(form.dateRanges[dateRangeName]['from']) }} + {{ 'chill_calendar.To'|trans }} + {{ form_widget(form.dateRanges[dateRangeName]['to']) }} +
+
+
+ {% endfor %} + {% endif %} {% endif %} - {% endif %} - {% if form.checkboxes is defined %} - {% set btnSubmit = 1 %} - {% if form.checkboxes|length > 0 %} - {% for checkbox_name, options in form.checkboxes %} + {% if form.checkboxes is defined %} + {% set btnSubmit = 1 %} + {% if form.checkboxes|length > 0 %} + {% for checkbox_name, options in form.checkboxes %} +
+
{{ 'activity_filter.By'|trans }}
+
+ {% for c in form['checkboxes'][checkbox_name].children %} + {{ form_widget(c) }} + {{ form_label(c) }} + {% endfor %} +
+
+ {% endfor %} + {% endif %} + {% endif %} + {% if form.entity_choices is defined %} + {% set btnSubmit = 1 %} + {% if form.entity_choices |length > 0 %} + {% for checkbox_name, options in form.entity_choices %} +
+ {% if form.entity_choices[checkbox_name].vars.label is not same as(false) %} + {{ form_label(form.entity_choices[checkbox_name])}} + {% endif %} +
+ {% for c in form['entity_choices'][checkbox_name].children %} + {{ form_widget(c) }} + {{ form_label(c) }} + {% endfor %} +
+
+ {% endfor %} + {% endif %} + {% endif %} + {% if form.single_checkboxes is defined %} + {% set btnSubmit = 1 %} + {% for name, _o in form.single_checkboxes %}
{{ 'activity_filter.By'|trans }}
- {% for c in form['checkboxes'][checkbox_name].children %} - {{ form_widget(c) }} - {{ form_label(c) }} - {% endfor %} + {{ form_widget(form.single_checkboxes[name]) }}
{% endfor %} {% endif %} - {% endif %} - {% if form.entity_choices is defined %} - {% set btnSubmit = 1 %} - {% if form.entity_choices |length > 0 %} - {% for checkbox_name, options in form.entity_choices %} -
- {% if form.entity_choices[checkbox_name].vars.label is not same as(false) %} - {{ form_label(form.entity_choices[checkbox_name])}} - {% endif %} -
- {% for c in form['entity_choices'][checkbox_name].children %} - {{ form_widget(c) }} - {{ form_label(c) }} - {% endfor %} -
-
- {% endfor %} - {% endif %} - {% endif %} - {% if form.single_checkboxes is defined %} - {% set btnSubmit = 1 %} - {% for name, _o in form.single_checkboxes %} + + {% if btnSubmit == 1 %}
-
{{ 'activity_filter.By'|trans }}
-
- {{ form_widget(form.single_checkboxes[name]) }} -
+
- {% endfor %} - {% endif %} - - {% if btnSubmit == 1 %} -
- -
- {% endif %} + {% endif %} +
+
- {% for k,v in otherParameters %} - - {% endfor %} +{% for k,v in otherParameters %} + +{% endfor %} {{ form_end(form) }} + From 6bdb3e969538bc3cf0ad3458d5015d3ab180626e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 7 Jul 2023 21:49:36 +0200 Subject: [PATCH 33/50] fix typo which prevent to apply a filter on activity types --- .../ChillActivityBundle/Controller/ActivityController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php index 9b6e69bf0..1e911ff08 100644 --- a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php +++ b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php @@ -259,7 +259,7 @@ final class ActivityController extends AbstractController $filterArgs = [ 'my_activities' => $filter->getSingleCheckboxData('my_activities'), - 'types' => $filter->hasEntityChoice('activity_type') ? $filter->getEntityChoiceData('activity_types') : [], + 'types' => $filter->hasEntityChoice('activity_types') ? $filter->getEntityChoiceData('activity_types') : [], 'jobs' => $filter->hasEntityChoice('jobs') ? $filter->getEntityChoiceData('jobs') : [], 'before' => $filter->getDateRangeData('activity_date')['to'], 'after' => $filter->getDateRangeData('activity_date')['from'], From 39896ea6e26e3cb2392830b2e18c9bb8f396cefe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 10 Jul 2023 15:26:54 +0200 Subject: [PATCH 34/50] [FilterOrder] add a method to get all the active filters --- .../Form/Type/Listing/FilterOrderType.php | 25 +++--- .../views/FilterOrder/base.html.twig | 13 ++- .../Templating/Listing/FilterOrderHelper.php | 79 ++++++++++++++++--- .../Listing/FilterOrderHelperBuilder.php | 10 ++- .../Listing/FilterOrderHelperFactory.php | 8 +- .../Listing/FilterOrderPositionEnum.php | 12 +++ .../translations/messages+intl-icu.fr.yaml | 5 ++ 7 files changed, 126 insertions(+), 26 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderPositionEnum.php diff --git a/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php b/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php index 1f373400c..d16ce3813 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php @@ -47,16 +47,7 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType $checkboxesBuilder = $builder->create('checkboxes', null, ['compound' => true]); foreach ($helper->getCheckboxes() as $name => $c) { - $choices = array_combine( - array_map(static function ($c, $t) { - if (null !== $t) { - return $t; - } - - return $c; - }, $c['choices'], $c['trans']), - $c['choices'] - ); + $choices = self::buildCheckboxChoices($c['choices'], $c['trans']); $checkboxesBuilder->add($name, ChoiceType::class, [ 'choices' => $choices, @@ -125,6 +116,20 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType } } + public static function buildCheckboxChoices(array $choices, array $trans = []): array + { + return array_combine( + array_map(static function ($c, $t) { + if (null !== $t) { + return $t; + } + + return $c; + }, $choices, $trans), + $choices + ); + } + public function buildView(FormView $view, FormInterface $form, array $options) { /** @var FilterOrderHelper $helper */ diff --git a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig index f642c82aa..4de7604cf 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig @@ -85,7 +85,7 @@
{% endfor %} {% endif %} - + {% if btnSubmit == 1 %}
@@ -93,6 +93,17 @@ {% endif %}
+ {% set active = helper.getActiveFilters() %} + {% if active|length > 0 %} +
+ {% for f in active %} + {% if f.label != '' %}{{ f.label|trans }} : {% endif %}{{ f.value }} + {% endfor %} +
+ {% endif %} +
+ +
{% for k,v in otherParameters %} diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php index c0ef1cd89..a038dfd71 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php @@ -13,16 +13,19 @@ namespace Chill\MainBundle\Templating\Listing; use Chill\MainBundle\Form\Type\Listing\FilterOrderType; use DateTimeImmutable; -use Symfony\Component\Form\Extension\Core\Type\FormType; -use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\PropertyAccess\PropertyAccessor; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyAccess\PropertyPath; +use Symfony\Component\PropertyAccess\PropertyPathInterface; +use Symfony\Contracts\Translation\TranslatorInterface; use function array_merge; use function count; -class FilterOrderHelper +final class FilterOrderHelper { private array $checkboxes = []; @@ -33,16 +36,12 @@ class FilterOrderHelper private array $dateRanges = []; - private FormFactoryInterface $formFactory; - public const FORM_NAME = 'f'; private array $formOptions = []; private string $formType = FilterOrderType::class; - private RequestStack $requestStack; - private ?array $searchBoxFields = null; private ?array $submitted = null; @@ -52,12 +51,13 @@ class FilterOrderHelper */ private array $entityChoices = []; + public function __construct( - FormFactoryInterface $formFactory, - RequestStack $requestStack + private readonly FormFactoryInterface $formFactory, + private readonly RequestStack $requestStack, + private readonly TranslatorInterface $translator, + private readonly PropertyAccessorInterface $propertyAccessor, ) { - $this->formFactory = $formFactory; - $this->requestStack = $requestStack; } public function addSingleCheckbox(string $name, string $label): self @@ -199,6 +199,63 @@ class FilterOrderHelper return $this; } + /** + * Return all the data required to display the active filters + * + * @return array + */ + public function getActiveFilters(): array + { + $result = []; + + if ($this->hasSearchBox() && '' !== $this->getQueryString()) { + $result[] = ['label' => '', 'value' => $this->getQueryString(), 'position' => FilterOrderPositionEnum::SearchBox->value, 'name' => 'q']; + } + + foreach ($this->dateRanges as $name => ['label' => $label]) { + $base = ['position' => FilterOrderPositionEnum::DateRange->value, 'name' => $name, 'label' => (string) $label]; + + if (null !== ($from = $this->getDateRangeData($name)['from'] ?? null)) { + $result[] = ['value' => $this->translator->trans('filter_order.by_date.From', ['from_date' => $from]), ...$base]; + } + if (null !== ($to = $this->getDateRangeData($name)['to'] ?? null)) { + $result[] = ['value' => $this->translator->trans('filter_order.by_date.To', ['to_date' => $to]), ...$base]; + } + } + + foreach ($this->checkboxes as $name => ['choices' => $choices, 'trans' => $trans, 'options' => $options]) { + foreach ($this->getCheckboxData($name) as $keyChoice) { + $result[] = ['value' => $choices['keyChoice'], 'label' => $options['label'], 'position' => FilterOrderPositionEnum::Checkboxes->value, 'name' => $name]; + } + } + + foreach ($this->entityChoices as $name => ['label' => $label, 'class' => $class, 'choices' => $choices, 'options' => $options]) { + foreach ($this->getEntityChoiceData($name) as $selected) { + if (is_callable($options['choice_label'])) { + $value = call_user_func($options['choice_label'], $selected); + } elseif ($options['choice_label'] instanceof PropertyPathInterface || is_string($options['choice_label'])) { + $value = $this->propertyAccessor->getValue($selected, $options['choice_label']); + } else { + if (!$selected instanceof \Stringable) { + throw new \UnexpectedValueException(sprintf("we are not able to transform the value of %s to a string. Implements \Stringable or add a 'choice_label' option to the filterFormBuilder", get_class($selected))); + } + + $value = (string) $selected; + } + + $result[] = ['value' => $value, 'label' => $label, 'position' => FilterOrderPositionEnum::EntityChoice->value, 'name' => $name]; + } + } + + foreach ($this->singleCheckbox as $name => ['label' => $label]) { + if (true === $this->getSingleCheckboxData($name)) { + $result[] = ['label' => '', 'value' => $this->translator->trans($label), 'position' => FilterOrderPositionEnum::SingleCheckbox->value, 'name' => $name]; + } + } + + return $result; + } + private function getDefaultData(): array { $r = [ diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php index e176e27c6..dfa361f6e 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php @@ -14,6 +14,8 @@ namespace Chill\MainBundle\Templating\Listing; use DateTimeImmutable; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; class FilterOrderHelperBuilder { @@ -39,7 +41,9 @@ class FilterOrderHelperBuilder public function __construct( FormFactoryInterface $formFactory, - RequestStack $requestStack + RequestStack $requestStack, + private readonly TranslatorInterface $translator, + private readonly PropertyAccessorInterface $propertyAccessor, ) { $this->formFactory = $formFactory; $this->requestStack = $requestStack; @@ -87,7 +91,9 @@ class FilterOrderHelperBuilder { $helper = new FilterOrderHelper( $this->formFactory, - $this->requestStack + $this->requestStack, + $this->translator, + $this->propertyAccessor ); $helper->setSearchBox($this->searchBoxFields); diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperFactory.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperFactory.php index c88c71af5..3fbb2864d 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperFactory.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperFactory.php @@ -13,6 +13,8 @@ namespace Chill\MainBundle\Templating\Listing; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; class FilterOrderHelperFactory implements FilterOrderHelperFactoryInterface { @@ -22,7 +24,9 @@ class FilterOrderHelperFactory implements FilterOrderHelperFactoryInterface public function __construct( FormFactoryInterface $formFactory, - RequestStack $requestStack + RequestStack $requestStack, + private readonly TranslatorInterface $translator, + private readonly PropertyAccessorInterface $propertyAccessor, ) { $this->formFactory = $formFactory; $this->requestStack = $requestStack; @@ -30,6 +34,6 @@ class FilterOrderHelperFactory implements FilterOrderHelperFactoryInterface public function create(string $context, ?array $options = []): FilterOrderHelperBuilder { - return new FilterOrderHelperBuilder($this->formFactory, $this->requestStack); + return new FilterOrderHelperBuilder($this->formFactory, $this->requestStack, $this->translator, $this->propertyAccessor); } } diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderPositionEnum.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderPositionEnum.php new file mode 100644 index 000000000..cda8119f5 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderPositionEnum.php @@ -0,0 +1,12 @@ + Date: Mon, 10 Jul 2023 15:39:00 +0200 Subject: [PATCH 35/50] [filterOrder] fix error in method getActiveFilters when dealing with entityChoice with incorrect number of translation --- .../Templating/Listing/FilterOrderHelper.php | 13 ++++++++----- .../Templating/Listing/FilterOrderPositionEnum.php | 9 +++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php index a038dfd71..701238208 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php @@ -84,9 +84,11 @@ final class FilterOrderHelper public function addCheckbox(string $name, array $choices, ?array $default = [], ?array $trans = [], array $options = []): self { - $missing = count($choices) - count($trans) - 1; + $missing = count($choices) - count($trans); + $this->checkboxes[$name] = [ - 'choices' => $choices, 'default' => $default, + 'choices' => $choices, + 'default' => $default, 'trans' => array_merge( $trans, 0 < $missing ? @@ -223,9 +225,10 @@ final class FilterOrderHelper } } - foreach ($this->checkboxes as $name => ['choices' => $choices, 'trans' => $trans, 'options' => $options]) { + foreach ($this->checkboxes as $name => ['choices' => $choices, 'trans' => $trans]) { + $translatedChoice = array_combine($choices, [...$trans]); foreach ($this->getCheckboxData($name) as $keyChoice) { - $result[] = ['value' => $choices['keyChoice'], 'label' => $options['label'], 'position' => FilterOrderPositionEnum::Checkboxes->value, 'name' => $name]; + $result[] = ['value' => $translatedChoice[$keyChoice], 'label' => '', 'position' => FilterOrderPositionEnum::Checkboxes->value, 'name' => $name]; } } @@ -237,7 +240,7 @@ final class FilterOrderHelper $value = $this->propertyAccessor->getValue($selected, $options['choice_label']); } else { if (!$selected instanceof \Stringable) { - throw new \UnexpectedValueException(sprintf("we are not able to transform the value of %s to a string. Implements \Stringable or add a 'choice_label' option to the filterFormBuilder", get_class($selected))); + throw new \UnexpectedValueException(sprintf("we are not able to transform the value of %s to a string. Implements \\Stringable or add a 'choice_label' option to the filterFormBuilder", get_class($selected))); } $value = (string) $selected; diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderPositionEnum.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderPositionEnum.php index cda8119f5..09e8d39aa 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderPositionEnum.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderPositionEnum.php @@ -1,5 +1,14 @@ Date: Mon, 10 Jul 2023 15:55:05 +0200 Subject: [PATCH 36/50] render active filters like pills --- .../Resources/views/FilterOrder/base.html.twig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig index 4de7604cf..7a05a2f80 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig @@ -95,9 +95,9 @@ {% set active = helper.getActiveFilters() %} {% if active|length > 0 %} -
+
{% for f in active %} - {% if f.label != '' %}{{ f.label|trans }} : {% endif %}{{ f.value }} + {% if f.label != '' %}{{ f.label|trans }} : {% endif %}{{ f.value }} {% endfor %}
{% endif %} From 0d365e16e58df1614b132fc4b1fa2dc655458e3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 10 Jul 2023 15:59:17 +0200 Subject: [PATCH 37/50] add missing translations --- .../ChillMainBundle/Templating/Listing/FilterOrderHelper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php index 701238208..2b24ffa0d 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php @@ -228,7 +228,7 @@ final class FilterOrderHelper foreach ($this->checkboxes as $name => ['choices' => $choices, 'trans' => $trans]) { $translatedChoice = array_combine($choices, [...$trans]); foreach ($this->getCheckboxData($name) as $keyChoice) { - $result[] = ['value' => $translatedChoice[$keyChoice], 'label' => '', 'position' => FilterOrderPositionEnum::Checkboxes->value, 'name' => $name]; + $result[] = ['value' => $this->translator->trans($translatedChoice[$keyChoice]), 'label' => '', 'position' => FilterOrderPositionEnum::Checkboxes->value, 'name' => $name]; } } @@ -246,7 +246,7 @@ final class FilterOrderHelper $value = (string) $selected; } - $result[] = ['value' => $value, 'label' => $label, 'position' => FilterOrderPositionEnum::EntityChoice->value, 'name' => $name]; + $result[] = ['value' => $this->translator->trans($value), 'label' => $label, 'position' => FilterOrderPositionEnum::EntityChoice->value, 'name' => $name]; } } From bf93c1ddb21f2bcbb28d42094b4f0d7c4cc78ef5 Mon Sep 17 00:00:00 2001 From: Mathieu Jaumotte Date: Tue, 11 Jul 2023 14:06:10 +0200 Subject: [PATCH 38/50] fix label color in active filters pills --- .../translations/messages.fr.yml | 3 --- .../Form/Type/Listing/FilterOrderType.php | 2 +- .../Resources/views/FilterOrder/base.html.twig | 16 ++++++++++++---- .../translations/messages+intl-icu.fr.yaml | 4 ++++ 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml index 3099e99b0..c53a04f31 100644 --- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml @@ -96,9 +96,6 @@ activity_filter: My activities: Mes échanges (où j'interviens) Types: Par type d'échange Jobs: Par métier impliqué - By: Filtrer par - Search: Chercher dans la liste - By date: Filtrer par date #timeline '%user% has done an %activity_type%': '%user% a effectué un échange de type "%activity_type%"' diff --git a/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php b/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php index d16ce3813..f22b6bfba 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php @@ -39,7 +39,7 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType 'label' => false, 'required' => false, 'attr' => [ - 'placeholder' => 'activity_filter.Search', + 'placeholder' => 'filter_order.Search', ] ]); } diff --git a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig index 7a05a2f80..0dcc4ce3f 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig @@ -26,7 +26,7 @@ {% if form.dateRanges[dateRangeName].vars.label is not same as(false) %} {{ form_label(form.dateRanges[dateRangeName])}} {% else %} -
{{ 'activity_filter.By date'|trans }}
+
{{ 'filter_order.By date'|trans }}
{% endif %}
@@ -45,7 +45,7 @@ {% if form.checkboxes|length > 0 %} {% for checkbox_name, options in form.checkboxes %}
-
{{ 'activity_filter.By'|trans }}
+
{{ 'filter_order.By'|trans }}
{% for c in form['checkboxes'][checkbox_name].children %} {{ form_widget(c) }} @@ -78,7 +78,7 @@ {% set btnSubmit = 1 %} {% for name, _o in form.single_checkboxes %}
-
{{ 'activity_filter.By'|trans }}
+
{{ 'filter_order.By'|trans }}
{{ form_widget(form.single_checkboxes[name]) }}
@@ -97,7 +97,15 @@ {% if active|length > 0 %}
{% for f in active %} - {% if f.label != '' %}{{ f.label|trans }} : {% endif %}{{ f.value }} + + {%- if f.label != '' %} + {{ f.label|trans }} : + {% endif -%} + {%- if f.position == 'search_box' and f.value is not null %} + {{ 'filter_order.search_box'|trans ~ ' :' }} + {% endif -%} + {{ f.value}}{# + #} {% endfor %}
{% endif %} diff --git a/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml b/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml index e1dc89dc2..96b2edd98 100644 --- a/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml +++ b/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml @@ -59,3 +59,7 @@ filter_order: by_date: From: Depuis le {from_date, date, long} To: Jusqu'au {to_date, date, long} + By: Filtrer par + Search: Chercher dans la liste + By date: Filtrer par date + search_box: Filtrer par contenu From 88114e3ba69fbd97a27eb850600acc78e27f526a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 11 Jul 2023 14:17:02 +0200 Subject: [PATCH 39/50] Fixed: [filterOrder] refactor active filter helper to a dedicated class and fix loading of multiple entity choices --- .../views/FilterOrder/base.html.twig | 1 - .../FilterOrderGetActiveFilterHelper.php | 84 +++++++++++++++++++ .../Templating/Listing/FilterOrderHelper.php | 70 +--------------- .../Listing/FilterOrderHelperBuilder.php | 4 - .../Listing/FilterOrderHelperFactory.php | 4 +- .../Templating/Listing/Templating.php | 2 + 6 files changed, 91 insertions(+), 74 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderGetActiveFilterHelper.php diff --git a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig index 0dcc4ce3f..b517eb154 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig @@ -93,7 +93,6 @@ {% endif %}
- {% set active = helper.getActiveFilters() %} {% if active|length > 0 %}
{% for f in active %} diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderGetActiveFilterHelper.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderGetActiveFilterHelper.php new file mode 100644 index 000000000..6b204e552 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderGetActiveFilterHelper.php @@ -0,0 +1,84 @@ + + */ + public function getActiveFilters(FilterOrderHelper $filterOrderHelper): array + { + $result = []; + + if ($filterOrderHelper->hasSearchBox() && '' !== $filterOrderHelper->getQueryString()) { + $result[] = ['label' => '', 'value' => $filterOrderHelper->getQueryString(), 'position' => FilterOrderPositionEnum::SearchBox->value, 'name' => 'q']; + } + + foreach ($filterOrderHelper->getDateRanges() as $name => ['label' => $label]) { + $base = ['position' => FilterOrderPositionEnum::DateRange->value, 'name' => $name, 'label' => (string)$label]; + + if (null !== ($from = $filterOrderHelper->getDateRangeData($name)['from'] ?? null)) { + $result[] = ['value' => $this->translator->trans('filter_order.by_date.From', ['from_date' => $from]), ...$base]; + } + if (null !== ($to = $filterOrderHelper->getDateRangeData($name)['to'] ?? null)) { + $result[] = ['value' => $this->translator->trans('filter_order.by_date.To', ['to_date' => $to]), ...$base]; + } + } + + foreach ($filterOrderHelper->getCheckboxes() as $name => ['choices' => $choices, 'trans' => $trans]) { + $translatedChoice = array_combine($choices, [...$trans]); + foreach ($filterOrderHelper->getCheckboxData($name) as $keyChoice) { + $result[] = ['value' => $this->translator->trans($translatedChoice[$keyChoice]), 'label' => '', 'position' => FilterOrderPositionEnum::Checkboxes->value, 'name' => $name]; + } + } + + foreach ($filterOrderHelper->getEntityChoices() as $name => ['label' => $label, 'class' => $class, 'choices' => $choices, 'options' => $options]) { + foreach ($filterOrderHelper->getEntityChoiceData($name) as $selected) { + if (is_callable($options['choice_label'])) { + $value = call_user_func($options['choice_label'], $selected); + } elseif ($options['choice_label'] instanceof PropertyPathInterface || is_string($options['choice_label'])) { + $value = $this->propertyAccessor->getValue($selected, $options['choice_label']); + } else { + if (!$selected instanceof \Stringable) { + throw new \UnexpectedValueException(sprintf("we are not able to transform the value of %s to a string. Implements \\Stringable or add a 'choice_label' option to the filterFormBuilder", get_class($selected))); + } + + $value = (string)$selected; + } + + $result[] = ['value' => $this->translator->trans($value), 'label' => $label, 'position' => FilterOrderPositionEnum::EntityChoice->value, 'name' => $name]; + } + } + + foreach ($filterOrderHelper->getSingleCheckbox() as $name => ['label' => $label]) { + if (true === $filterOrderHelper->getSingleCheckboxData($name)) { + $result[] = ['label' => '', 'value' => $this->translator->trans($label), 'position' => FilterOrderPositionEnum::SingleCheckbox->value, 'name' => $name]; + } + } + + return $result; + } +} diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php index 2b24ffa0d..84939a052 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php @@ -55,8 +55,6 @@ final class FilterOrderHelper public function __construct( private readonly FormFactoryInterface $formFactory, private readonly RequestStack $requestStack, - private readonly TranslatorInterface $translator, - private readonly PropertyAccessorInterface $propertyAccessor, ) { } @@ -84,16 +82,14 @@ final class FilterOrderHelper public function addCheckbox(string $name, array $choices, ?array $default = [], ?array $trans = [], array $options = []): self { - $missing = count($choices) - count($trans); + if ([] === $trans) { + $trans = $choices; + } $this->checkboxes[$name] = [ 'choices' => $choices, 'default' => $default, - 'trans' => array_merge( - $trans, - 0 < $missing ? - array_fill(0, $missing, null) : [] - ), + 'trans' => $trans, ...$options, ]; @@ -201,64 +197,6 @@ final class FilterOrderHelper return $this; } - /** - * Return all the data required to display the active filters - * - * @return array - */ - public function getActiveFilters(): array - { - $result = []; - - if ($this->hasSearchBox() && '' !== $this->getQueryString()) { - $result[] = ['label' => '', 'value' => $this->getQueryString(), 'position' => FilterOrderPositionEnum::SearchBox->value, 'name' => 'q']; - } - - foreach ($this->dateRanges as $name => ['label' => $label]) { - $base = ['position' => FilterOrderPositionEnum::DateRange->value, 'name' => $name, 'label' => (string) $label]; - - if (null !== ($from = $this->getDateRangeData($name)['from'] ?? null)) { - $result[] = ['value' => $this->translator->trans('filter_order.by_date.From', ['from_date' => $from]), ...$base]; - } - if (null !== ($to = $this->getDateRangeData($name)['to'] ?? null)) { - $result[] = ['value' => $this->translator->trans('filter_order.by_date.To', ['to_date' => $to]), ...$base]; - } - } - - foreach ($this->checkboxes as $name => ['choices' => $choices, 'trans' => $trans]) { - $translatedChoice = array_combine($choices, [...$trans]); - foreach ($this->getCheckboxData($name) as $keyChoice) { - $result[] = ['value' => $this->translator->trans($translatedChoice[$keyChoice]), 'label' => '', 'position' => FilterOrderPositionEnum::Checkboxes->value, 'name' => $name]; - } - } - - foreach ($this->entityChoices as $name => ['label' => $label, 'class' => $class, 'choices' => $choices, 'options' => $options]) { - foreach ($this->getEntityChoiceData($name) as $selected) { - if (is_callable($options['choice_label'])) { - $value = call_user_func($options['choice_label'], $selected); - } elseif ($options['choice_label'] instanceof PropertyPathInterface || is_string($options['choice_label'])) { - $value = $this->propertyAccessor->getValue($selected, $options['choice_label']); - } else { - if (!$selected instanceof \Stringable) { - throw new \UnexpectedValueException(sprintf("we are not able to transform the value of %s to a string. Implements \\Stringable or add a 'choice_label' option to the filterFormBuilder", get_class($selected))); - } - - $value = (string) $selected; - } - - $result[] = ['value' => $this->translator->trans($value), 'label' => $label, 'position' => FilterOrderPositionEnum::EntityChoice->value, 'name' => $name]; - } - } - - foreach ($this->singleCheckbox as $name => ['label' => $label]) { - if (true === $this->getSingleCheckboxData($name)) { - $result[] = ['label' => '', 'value' => $this->translator->trans($label), 'position' => FilterOrderPositionEnum::SingleCheckbox->value, 'name' => $name]; - } - } - - return $result; - } - private function getDefaultData(): array { $r = [ diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php index dfa361f6e..f2bded220 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php @@ -42,8 +42,6 @@ class FilterOrderHelperBuilder public function __construct( FormFactoryInterface $formFactory, RequestStack $requestStack, - private readonly TranslatorInterface $translator, - private readonly PropertyAccessorInterface $propertyAccessor, ) { $this->formFactory = $formFactory; $this->requestStack = $requestStack; @@ -92,8 +90,6 @@ class FilterOrderHelperBuilder $helper = new FilterOrderHelper( $this->formFactory, $this->requestStack, - $this->translator, - $this->propertyAccessor ); $helper->setSearchBox($this->searchBoxFields); diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperFactory.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperFactory.php index 3fbb2864d..6665750dd 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperFactory.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperFactory.php @@ -25,8 +25,6 @@ class FilterOrderHelperFactory implements FilterOrderHelperFactoryInterface public function __construct( FormFactoryInterface $formFactory, RequestStack $requestStack, - private readonly TranslatorInterface $translator, - private readonly PropertyAccessorInterface $propertyAccessor, ) { $this->formFactory = $formFactory; $this->requestStack = $requestStack; @@ -34,6 +32,6 @@ class FilterOrderHelperFactory implements FilterOrderHelperFactoryInterface public function create(string $context, ?array $options = []): FilterOrderHelperBuilder { - return new FilterOrderHelperBuilder($this->formFactory, $this->requestStack, $this->translator, $this->propertyAccessor); + return new FilterOrderHelperBuilder($this->formFactory, $this->requestStack); } } diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/Templating.php b/src/Bundle/ChillMainBundle/Templating/Listing/Templating.php index b91cd86e8..2d32813cb 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/Templating.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/Templating.php @@ -24,6 +24,7 @@ class Templating extends AbstractExtension { public function __construct( private readonly RequestStack $requestStack, + private readonly FilterOrderGetActiveFilterHelper $filterOrderGetActiveFilterHelper, ) { } @@ -68,6 +69,7 @@ class Templating extends AbstractExtension return $environment->render($template, [ 'helper' => $helper, + 'active' => $this->filterOrderGetActiveFilterHelper->getActiveFilters($helper), 'form' => $helper->buildForm()->createView(), 'options' => $options, 'otherParameters' => $otherParameters, From 6065680e1e0bc71c19dd05ef158d301324f61805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 11 Jul 2023 15:01:32 +0200 Subject: [PATCH 40/50] Feature: [export] allow to group activities by location --- .../unreleased/Feature-20230711-150055.yaml | 5 ++ .../Aggregator/ActivityLocationAggregator.php | 80 +++++++++++++++++++ .../config/services/export.yaml | 4 + .../translations/messages.fr.yml | 3 + 4 files changed, 92 insertions(+) create mode 100644 .changes/unreleased/Feature-20230711-150055.yaml create mode 100644 src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityLocationAggregator.php diff --git a/.changes/unreleased/Feature-20230711-150055.yaml b/.changes/unreleased/Feature-20230711-150055.yaml new file mode 100644 index 000000000..ecee61b49 --- /dev/null +++ b/.changes/unreleased/Feature-20230711-150055.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: '[Export] allow to group activities by localisation' +time: 2023-07-11T15:00:55.770070399+02:00 +custom: + Issue: "128" diff --git a/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityLocationAggregator.php b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityLocationAggregator.php new file mode 100644 index 000000000..9103943e4 --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Export/Aggregator/ActivityLocationAggregator.php @@ -0,0 +1,80 @@ +getAllAliases(), true)) { + $qb->leftJoin('activity.location', 'actloc'); + } + $qb->addSelect(sprintf('actloc.name AS %s', self::KEY)); + $qb->addGroupBy(self::KEY); + } + + public function applyOn(): string + { + return Declarations::ACTIVITY; + } + + 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 ($value): string { + if ('_header' === $value) { + return 'export.aggregator.activity.by_location.Activity Location'; + } + + if (null === $value || '' === $value) { + return ''; + } + + return $value; + }; + } + + public function getQueryKeys($data): array + { + return [self::KEY]; + } + + public function getTitle() + { + return 'export.aggregator.activity.by_location.Title'; + } +} diff --git a/src/Bundle/ChillActivityBundle/config/services/export.yaml b/src/Bundle/ChillActivityBundle/config/services/export.yaml index 09817d80e..03285c416 100644 --- a/src/Bundle/ChillActivityBundle/config/services/export.yaml +++ b/src/Bundle/ChillActivityBundle/config/services/export.yaml @@ -144,6 +144,10 @@ services: tags: - { name: chill.export_aggregator, alias: activity_common_type_aggregator } + Chill\ActivityBundle\Export\Aggregator\ActivityLocationAggregator: + tags: + - { name: chill.export_aggregator, alias: activity_common_location_aggregator } + chill.activity.export.user_aggregator: class: Chill\ActivityBundle\Export\Aggregator\ActivityUserAggregator tags: diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml index abef160d3..037c24f3f 100644 --- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml @@ -372,6 +372,9 @@ export: is sent: envoyé is received: reçu Group activity by sentreceived: Grouper les échanges par envoyé / reçu + by_location: + Activity Location: Localisation de l'échange + Title: Grouper les échanges par localisation de l'échange generic_doc: filter: From 2882038efcd4be2b65db67df7cb4f2498fcb0f57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 11 Jul 2023 16:00:40 +0200 Subject: [PATCH 41/50] [export] Add a filter "filter course having an activity between two dates" --- .../unreleased/Feature-20230711-155929.yaml | 5 ++ ...PeriodHavingActivityBetweenDatesFilter.php | 90 +++++++++++++++++++ .../config/services/export.yaml | 4 + .../translations/messages+intl-icu.fr.yml | 5 ++ .../translations/messages.fr.yml | 6 ++ 5 files changed, 110 insertions(+) create mode 100644 .changes/unreleased/Feature-20230711-155929.yaml create mode 100644 src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/PeriodHavingActivityBetweenDatesFilter.php create mode 100644 src/Bundle/ChillActivityBundle/translations/messages+intl-icu.fr.yml diff --git a/.changes/unreleased/Feature-20230711-155929.yaml b/.changes/unreleased/Feature-20230711-155929.yaml new file mode 100644 index 000000000..329bbb677 --- /dev/null +++ b/.changes/unreleased/Feature-20230711-155929.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: '[export] Add a filter "filter course having an activity between two dates"' +time: 2023-07-11T15:59:29.065329834+02:00 +custom: + Issue: "129" diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/PeriodHavingActivityBetweenDatesFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/PeriodHavingActivityBetweenDatesFilter.php new file mode 100644 index 000000000..27e012d0b --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/PeriodHavingActivityBetweenDatesFilter.php @@ -0,0 +1,90 @@ +add('start_date', PickRollingDateType::class, [ + 'label' => 'export.filter.activity.course_having_activity_between_date.Receiving an activity after' + ]) + ->add('end_date', PickRollingDateType::class, [ + 'label' => 'export.filter.activity.course_having_activity_between_date.Receiving an activity before' + ]); + } + + public function getFormDefaultData(): array + { + return [ + 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), + 'end_date' => new RollingDate(RollingDate::T_TODAY) + ]; + } + + public function describeAction($data, $format = 'string') + { + return [ + 'export.filter.activity.course_having_activity_between_date.Only course having an activity between from and to', + [ + 'from' => $this->rollingDateConverter->convert($data['start_date']), + 'to' => $this->rollingDateConverter->convert($data['end_date']), + ] + ]; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $alias = 'act_period_having_act_betw_date_alias'; + $from = 'act_period_having_act_betw_date_start'; + $to = 'act_period_having_act_betw_date_end'; + + $qb->andWhere( + $qb->expr()->exists( + 'SELECT 1 FROM ' . Activity::class . " {$alias} WHERE {$alias}.date >= :{$from} AND {$alias}.date < :{$to} AND {$alias}.accompanyingPeriod = acp" + ) + ); + + $qb + ->setParameter($from, $this->rollingDateConverter->convert($data['start_date'])) + ->setParameter($to, $this->rollingDateConverter->convert($data['end_date'])); + } + + public function applyOn() + { + return \Chill\PersonBundle\Export\Declarations::ACP_TYPE; + } +} diff --git a/src/Bundle/ChillActivityBundle/config/services/export.yaml b/src/Bundle/ChillActivityBundle/config/services/export.yaml index 09817d80e..932985083 100644 --- a/src/Bundle/ChillActivityBundle/config/services/export.yaml +++ b/src/Bundle/ChillActivityBundle/config/services/export.yaml @@ -135,6 +135,10 @@ services: tags: - { name: chill.export_filter, alias: 'accompanyingcourse_has_no_activity_filter' } + Chill\ActivityBundle\Export\Filter\ACPFilters\PeriodHavingActivityBetweenDatesFilter: + tags: + - { name: chill.export_filter, alias: 'period_having_activity_betw_dates_filter' } + ## Aggregators Chill\ActivityBundle\Export\Aggregator\PersonAggregators\ActivityReasonAggregator: tags: diff --git a/src/Bundle/ChillActivityBundle/translations/messages+intl-icu.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages+intl-icu.fr.yml new file mode 100644 index 000000000..ab3b963ab --- /dev/null +++ b/src/Bundle/ChillActivityBundle/translations/messages+intl-icu.fr.yml @@ -0,0 +1,5 @@ +export: + filter: + activity: + course_having_activity_between_date: + Only course having an activity between from and to: Seulement les parcours ayant reçu au moins un échange entre le {from, date, short} et le {to, date, short} diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml index abef160d3..551a63d27 100644 --- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml @@ -365,6 +365,12 @@ export: by_usersscope: Filter by users scope: Filtrer les échanges par services d'au moins un utilisateur participant 'Filtered activity by users scope: only %scopes%': 'Filtré par service d''au moins un utilisateur participant: seulement %scopes%' + course_having_activity_between_date: + Title: Filtre les parcours ayant reçu un échange entre deux dates + Receiving an activity after: Ayant reçu un échange après le + Receiving an activity before: Ayant reçu un échange avant le + + aggregator: activity: by_sent_received: From edd66f6a6cf3622dd82a8535a9f4a6272cc9bb1d Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 12 Jul 2023 09:04:15 +0200 Subject: [PATCH 42/50] FIX [budget][templates] reimplement display of all calculator results --- .../Resources/views/Budget/_macros.html.twig | 33 ++++++++++++++----- .../Resources/views/Person/index.html.twig | 2 +- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/Bundle/ChillBudgetBundle/Resources/views/Budget/_macros.html.twig b/src/Bundle/ChillBudgetBundle/Resources/views/Budget/_macros.html.twig index dfa286af4..a1fee19ce 100644 --- a/src/Bundle/ChillBudgetBundle/Resources/views/Budget/_macros.html.twig +++ b/src/Bundle/ChillBudgetBundle/Resources/views/Budget/_macros.html.twig @@ -1,11 +1,12 @@ -{% macro table_elements(elements, family) %} +{% macro table_elements(elements, type) %} + - - - - + + + + @@ -38,17 +39,17 @@
    {% if is_granted('CHILL_BUDGET_ELEMENT_SEE', f) %}
  • - +
  • {% endif %} {% if is_granted('CHILL_BUDGET_ELEMENT_UPDATE', f) %}
  • - +
  • {% endif %} {% if is_granted('CHILL_BUDGET_ELEMENT_DELETE', f) %}
  • - +
  • {% endif %}
@@ -69,7 +70,7 @@
{{ 'Budget element type'|trans }}{{ 'Amount'|trans }}{{ 'Validity period'|trans }} {{ 'Budget element type'|trans }}{{ 'Amount'|trans }}{{ 'Validity period'|trans }} 
{% endmacro %} -{% macro table_results(actualCharges, actualResources) %} +{% macro table_results(actualCharges, actualResources, results) %} {% set totalCharges = 0 %} {% for c in actualCharges %} @@ -97,6 +98,20 @@ {{ result|format_currency('EUR') }} + {% for result in results %} + + {{ result.label }} + + {% if result.type == 'currency' %} + {{ result.result|format_currency('EUR') }} + {% elseif result.type == 'percentage' %} + {{ result.result|round(2, 'ceil') ~ '%' }} + {% else %} + {{ result.result|round(2, 'common') }} + {% endif %} + + + {% endfor %} {% endmacro %} diff --git a/src/Bundle/ChillBudgetBundle/Resources/views/Person/index.html.twig b/src/Bundle/ChillBudgetBundle/Resources/views/Person/index.html.twig index 18d04b889..aba564206 100644 --- a/src/Bundle/ChillBudgetBundle/Resources/views/Person/index.html.twig +++ b/src/Bundle/ChillBudgetBundle/Resources/views/Person/index.html.twig @@ -25,7 +25,7 @@

{{ 'Budget calculator'|trans }}

- {{ table_results(charges, resources) }} + {{ table_results(charges, resources, results) }}
{% if is_granted('CHILL_BUDGET_ELEMENT_CREATE', person) %} From f7d385eba1877756c34ed64857d0e847aedccf9d Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 12 Jul 2023 09:06:08 +0200 Subject: [PATCH 43/50] DX add changie --- .changes/unreleased/Fixed-20230712-090514.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changes/unreleased/Fixed-20230712-090514.yaml diff --git a/.changes/unreleased/Fixed-20230712-090514.yaml b/.changes/unreleased/Fixed-20230712-090514.yaml new file mode 100644 index 000000000..51a8b9317 --- /dev/null +++ b/.changes/unreleased/Fixed-20230712-090514.yaml @@ -0,0 +1,5 @@ +kind: Fixed +body: reimplement the visualization of all calculator results (specific to AMLI) +time: 2023-07-12T09:05:14.416268226+02:00 +custom: + Issue: "" From a2e705bd92e085007fb0313b163f194497bcb5bf Mon Sep 17 00:00:00 2001 From: Mathieu Jaumotte Date: Wed, 12 Jul 2023 10:38:11 +0200 Subject: [PATCH 44/50] fixed: error with parent joins in thirdparty api search query --- .../Controller/resquery.bad.sql | 21 +++++++++++++++++++ .../Controller/resquery.fixed.sql | 21 +++++++++++++++++++ .../Search/ThirdPartyApiSearch.php | 2 +- 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 src/Bundle/ChillMainBundle/Controller/resquery.bad.sql create mode 100644 src/Bundle/ChillMainBundle/Controller/resquery.fixed.sql diff --git a/src/Bundle/ChillMainBundle/Controller/resquery.bad.sql b/src/Bundle/ChillMainBundle/Controller/resquery.bad.sql new file mode 100644 index 000000000..1033ec28c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/resquery.bad.sql @@ -0,0 +1,21 @@ +SELECT +'tparty' AS key, jsonb_build_object('id', tparty.id) AS metadata, GREATEST( +STRICT_WORD_SIMILARITY(LOWER(UNACCENT('areams')), tparty.canonicalized), +STRICT_WORD_SIMILARITY(LOWER(UNACCENT('areams')), parent.canonicalized) +) + GREATEST( +(tparty.canonicalized LIKE '%s' || LOWER(UNACCENT('areams')) || '%')::int, +(parent.canonicalized LIKE '%s' || LOWER(UNACCENT('areams')) || '%')::int +) + COALESCE((LOWER(UNACCENT(cmpc.label)) LIKE '%' || LOWER(UNACCENT('areams')) || '%')::int * 0.3, 0) + COALESCE((LOWER(UNACCENT(cmpc_p.label)) LIKE '%' || LOWER(UNACCENT('areams')) || '%')::int * 0.3, 0) + 1 AS pertinence +FROM chill_3party.third_party AS tparty +LEFT JOIN chill_main_address cma ON cma.id = tparty.address_id +LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id +LEFT JOIN chill_3party.third_party AS parent ON tparty.parent_id = parent.id +LEFT JOIN chill_main_address cma_p ON parent.address_id = cma_p.id +LEFT JOIN chill_main_postal_code cmpc_p ON cma_p.postcode_id = cmpc.id +WHERE (tparty.active IS TRUE) AND (((LOWER(UNACCENT('areams')) <<% tparty.canonicalized OR +tparty.canonicalized LIKE '%' || LOWER(UNACCENT('areams')) || '%') +OR +(LOWER(UNACCENT('areams')) <<% parent.canonicalized OR +parent.canonicalized LIKE '%' || LOWER(UNACCENT('areams')) || '%')) +AND tparty.active IS TRUE and (parent.active IS TRUE OR parent IS NULL)) +ORDER BY pertinence DESC LIMIT 50 OFFSET 0; diff --git a/src/Bundle/ChillMainBundle/Controller/resquery.fixed.sql b/src/Bundle/ChillMainBundle/Controller/resquery.fixed.sql new file mode 100644 index 000000000..dbb55f187 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/resquery.fixed.sql @@ -0,0 +1,21 @@ +SELECT +'tparty' AS key, jsonb_build_object('id', tparty.id) AS metadata, GREATEST( +STRICT_WORD_SIMILARITY(LOWER(UNACCENT('areams')), tparty.canonicalized), +STRICT_WORD_SIMILARITY(LOWER(UNACCENT('areams')), parent.canonicalized) +) + GREATEST( +(tparty.canonicalized LIKE '%s' || LOWER(UNACCENT('areams')) || '%')::int, +(parent.canonicalized LIKE '%s' || LOWER(UNACCENT('areams')) || '%')::int +) + COALESCE((LOWER(UNACCENT(cmpc.label)) LIKE '%' || LOWER(UNACCENT('areams')) || '%')::int * 0.3, 0) + COALESCE((LOWER(UNACCENT(cmpc_p.label)) LIKE '%' || LOWER(UNACCENT('areams')) || '%')::int * 0.3, 0) + 1 AS pertinence +FROM chill_3party.third_party AS tparty +LEFT JOIN chill_main_address cma ON cma.id = tparty.address_id +LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id +LEFT JOIN chill_3party.third_party AS parent ON tparty.parent_id = parent.id +LEFT JOIN chill_main_address cma_p ON parent.address_id = cma_p.id +LEFT JOIN chill_main_postal_code cmpc_p ON cma_p.postcode_id = cmpc_p.id +WHERE (tparty.active IS TRUE) AND (((LOWER(UNACCENT('areams')) <<% tparty.canonicalized OR +tparty.canonicalized LIKE '%' || LOWER(UNACCENT('areams')) || '%') +OR +(LOWER(UNACCENT('areams')) <<% parent.canonicalized OR +parent.canonicalized LIKE '%' || LOWER(UNACCENT('areams')) || '%')) +AND tparty.active IS TRUE and (parent.active IS TRUE OR parent IS NULL)) +ORDER BY pertinence DESC LIMIT 50 OFFSET 0; diff --git a/src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php b/src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php index 86c0fa9db..42b98622f 100644 --- a/src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php +++ b/src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php @@ -75,7 +75,7 @@ class ThirdPartyApiSearch implements SearchApiInterface LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id LEFT JOIN chill_3party.third_party AS parent ON tparty.parent_id = parent.id LEFT JOIN chill_main_address cma_p ON parent.address_id = cma_p.id - LEFT JOIN chill_main_postal_code cmpc_p ON cma_p.postcode_id = cmpc.id') + LEFT JOIN chill_main_postal_code cmpc_p ON cma_p.postcode_id = cmpc_p.id') ->andWhereClause('tparty.active IS TRUE'); $strs = explode(' ', $pattern); From f3829d3390ae7e22939f02a6bb71db305ebe060e Mon Sep 17 00:00:00 2001 From: Mathieu Jaumotte Date: Wed, 12 Jul 2023 10:50:17 +0200 Subject: [PATCH 45/50] adapt query to simplify join clauses (lightly improve perfs) --- .../Controller/resquery.good.sql | 19 +++++++++++++++++++ .../Search/ThirdPartyApiSearch.php | 10 +++------- 2 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Controller/resquery.good.sql diff --git a/src/Bundle/ChillMainBundle/Controller/resquery.good.sql b/src/Bundle/ChillMainBundle/Controller/resquery.good.sql new file mode 100644 index 000000000..5877b04f8 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/resquery.good.sql @@ -0,0 +1,19 @@ +SELECT +'tparty' AS key, jsonb_build_object('id', tparty.id) AS metadata, GREATEST( +STRICT_WORD_SIMILARITY(LOWER(UNACCENT('areams')), tparty.canonicalized), +STRICT_WORD_SIMILARITY(LOWER(UNACCENT('areams')), parent.canonicalized) +) + GREATEST( +(tparty.canonicalized LIKE '%s' || LOWER(UNACCENT('areams')) || '%')::int, +(parent.canonicalized LIKE '%s' || LOWER(UNACCENT('areams')) || '%')::int +) + COALESCE((LOWER(UNACCENT(cmpc.label)) LIKE '%' || LOWER(UNACCENT('areams')) || '%')::int * 0.3, 0) + 1 AS pertinence +FROM chill_3party.third_party AS tparty +LEFT JOIN chill_3party.third_party AS parent ON tparty.parent_id = parent.id +LEFT JOIN chill_main_address cma ON cma.id = COALESCE(parent.address_id, tparty.address_id) +LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id +WHERE (((LOWER(UNACCENT('areams')) <<% tparty.canonicalized OR +tparty.canonicalized LIKE '%' || LOWER(UNACCENT('areams')) || '%') +OR +(LOWER(UNACCENT('areams')) <<% parent.canonicalized OR +parent.canonicalized LIKE '%' || LOWER(UNACCENT('areams')) || '%')) +AND tparty.active IS TRUE and (parent.active IS TRUE OR parent IS NULL)) +ORDER BY pertinence DESC, tparty.id ASC LIMIT 500 OFFSET 0; diff --git a/src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php b/src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php index 42b98622f..bb5303143 100644 --- a/src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php +++ b/src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php @@ -71,12 +71,9 @@ class ThirdPartyApiSearch implements SearchApiInterface ->setSelectKey('tparty') ->setSelectJsonbMetadata("jsonb_build_object('id', tparty.id)") ->setFromClause('chill_3party.third_party AS tparty - LEFT JOIN chill_main_address cma ON cma.id = tparty.address_id - LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id LEFT JOIN chill_3party.third_party AS parent ON tparty.parent_id = parent.id - LEFT JOIN chill_main_address cma_p ON parent.address_id = cma_p.id - LEFT JOIN chill_main_postal_code cmpc_p ON cma_p.postcode_id = cmpc_p.id') - ->andWhereClause('tparty.active IS TRUE'); + LEFT JOIN chill_main_address cma ON cma.id = COALESCE(parent.address_id, tparty.address_id) + LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id'); $strs = explode(' ', $pattern); $wheres = []; @@ -102,8 +99,7 @@ class ThirdPartyApiSearch implements SearchApiInterface (parent.canonicalized LIKE '%s' || LOWER(UNACCENT(?)) || '%')::int ) + " . // take postcode label into account, but lower than the canonicalized field - "COALESCE((LOWER(UNACCENT(cmpc.label)) LIKE '%' || LOWER(UNACCENT(?)) || '%')::int * 0.3, 0) + " . - "COALESCE((LOWER(UNACCENT(cmpc_p.label)) LIKE '%' || LOWER(UNACCENT(?)) || '%')::int * 0.3, 0)"; + "COALESCE((LOWER(UNACCENT(cmpc.label)) LIKE '%' || LOWER(UNACCENT(?)) || '%')::int * 0.3, 0)"; $pertinenceArgs[] = [$str, $str, $str, $str, $str, $str]; } } From efee2d8b44369e6546d13afa19d58b8a43d086db Mon Sep 17 00:00:00 2001 From: Mathieu Jaumotte Date: Wed, 12 Jul 2023 10:53:12 +0200 Subject: [PATCH 46/50] cleaning --- .../Controller/resquery.bad.sql | 21 ------------------- .../Controller/resquery.fixed.sql | 21 ------------------- .../Controller/resquery.good.sql | 19 ----------------- 3 files changed, 61 deletions(-) delete mode 100644 src/Bundle/ChillMainBundle/Controller/resquery.bad.sql delete mode 100644 src/Bundle/ChillMainBundle/Controller/resquery.fixed.sql delete mode 100644 src/Bundle/ChillMainBundle/Controller/resquery.good.sql diff --git a/src/Bundle/ChillMainBundle/Controller/resquery.bad.sql b/src/Bundle/ChillMainBundle/Controller/resquery.bad.sql deleted file mode 100644 index 1033ec28c..000000000 --- a/src/Bundle/ChillMainBundle/Controller/resquery.bad.sql +++ /dev/null @@ -1,21 +0,0 @@ -SELECT -'tparty' AS key, jsonb_build_object('id', tparty.id) AS metadata, GREATEST( -STRICT_WORD_SIMILARITY(LOWER(UNACCENT('areams')), tparty.canonicalized), -STRICT_WORD_SIMILARITY(LOWER(UNACCENT('areams')), parent.canonicalized) -) + GREATEST( -(tparty.canonicalized LIKE '%s' || LOWER(UNACCENT('areams')) || '%')::int, -(parent.canonicalized LIKE '%s' || LOWER(UNACCENT('areams')) || '%')::int -) + COALESCE((LOWER(UNACCENT(cmpc.label)) LIKE '%' || LOWER(UNACCENT('areams')) || '%')::int * 0.3, 0) + COALESCE((LOWER(UNACCENT(cmpc_p.label)) LIKE '%' || LOWER(UNACCENT('areams')) || '%')::int * 0.3, 0) + 1 AS pertinence -FROM chill_3party.third_party AS tparty -LEFT JOIN chill_main_address cma ON cma.id = tparty.address_id -LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id -LEFT JOIN chill_3party.third_party AS parent ON tparty.parent_id = parent.id -LEFT JOIN chill_main_address cma_p ON parent.address_id = cma_p.id -LEFT JOIN chill_main_postal_code cmpc_p ON cma_p.postcode_id = cmpc.id -WHERE (tparty.active IS TRUE) AND (((LOWER(UNACCENT('areams')) <<% tparty.canonicalized OR -tparty.canonicalized LIKE '%' || LOWER(UNACCENT('areams')) || '%') -OR -(LOWER(UNACCENT('areams')) <<% parent.canonicalized OR -parent.canonicalized LIKE '%' || LOWER(UNACCENT('areams')) || '%')) -AND tparty.active IS TRUE and (parent.active IS TRUE OR parent IS NULL)) -ORDER BY pertinence DESC LIMIT 50 OFFSET 0; diff --git a/src/Bundle/ChillMainBundle/Controller/resquery.fixed.sql b/src/Bundle/ChillMainBundle/Controller/resquery.fixed.sql deleted file mode 100644 index dbb55f187..000000000 --- a/src/Bundle/ChillMainBundle/Controller/resquery.fixed.sql +++ /dev/null @@ -1,21 +0,0 @@ -SELECT -'tparty' AS key, jsonb_build_object('id', tparty.id) AS metadata, GREATEST( -STRICT_WORD_SIMILARITY(LOWER(UNACCENT('areams')), tparty.canonicalized), -STRICT_WORD_SIMILARITY(LOWER(UNACCENT('areams')), parent.canonicalized) -) + GREATEST( -(tparty.canonicalized LIKE '%s' || LOWER(UNACCENT('areams')) || '%')::int, -(parent.canonicalized LIKE '%s' || LOWER(UNACCENT('areams')) || '%')::int -) + COALESCE((LOWER(UNACCENT(cmpc.label)) LIKE '%' || LOWER(UNACCENT('areams')) || '%')::int * 0.3, 0) + COALESCE((LOWER(UNACCENT(cmpc_p.label)) LIKE '%' || LOWER(UNACCENT('areams')) || '%')::int * 0.3, 0) + 1 AS pertinence -FROM chill_3party.third_party AS tparty -LEFT JOIN chill_main_address cma ON cma.id = tparty.address_id -LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id -LEFT JOIN chill_3party.third_party AS parent ON tparty.parent_id = parent.id -LEFT JOIN chill_main_address cma_p ON parent.address_id = cma_p.id -LEFT JOIN chill_main_postal_code cmpc_p ON cma_p.postcode_id = cmpc_p.id -WHERE (tparty.active IS TRUE) AND (((LOWER(UNACCENT('areams')) <<% tparty.canonicalized OR -tparty.canonicalized LIKE '%' || LOWER(UNACCENT('areams')) || '%') -OR -(LOWER(UNACCENT('areams')) <<% parent.canonicalized OR -parent.canonicalized LIKE '%' || LOWER(UNACCENT('areams')) || '%')) -AND tparty.active IS TRUE and (parent.active IS TRUE OR parent IS NULL)) -ORDER BY pertinence DESC LIMIT 50 OFFSET 0; diff --git a/src/Bundle/ChillMainBundle/Controller/resquery.good.sql b/src/Bundle/ChillMainBundle/Controller/resquery.good.sql deleted file mode 100644 index 5877b04f8..000000000 --- a/src/Bundle/ChillMainBundle/Controller/resquery.good.sql +++ /dev/null @@ -1,19 +0,0 @@ -SELECT -'tparty' AS key, jsonb_build_object('id', tparty.id) AS metadata, GREATEST( -STRICT_WORD_SIMILARITY(LOWER(UNACCENT('areams')), tparty.canonicalized), -STRICT_WORD_SIMILARITY(LOWER(UNACCENT('areams')), parent.canonicalized) -) + GREATEST( -(tparty.canonicalized LIKE '%s' || LOWER(UNACCENT('areams')) || '%')::int, -(parent.canonicalized LIKE '%s' || LOWER(UNACCENT('areams')) || '%')::int -) + COALESCE((LOWER(UNACCENT(cmpc.label)) LIKE '%' || LOWER(UNACCENT('areams')) || '%')::int * 0.3, 0) + 1 AS pertinence -FROM chill_3party.third_party AS tparty -LEFT JOIN chill_3party.third_party AS parent ON tparty.parent_id = parent.id -LEFT JOIN chill_main_address cma ON cma.id = COALESCE(parent.address_id, tparty.address_id) -LEFT JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id -WHERE (((LOWER(UNACCENT('areams')) <<% tparty.canonicalized OR -tparty.canonicalized LIKE '%' || LOWER(UNACCENT('areams')) || '%') -OR -(LOWER(UNACCENT('areams')) <<% parent.canonicalized OR -parent.canonicalized LIKE '%' || LOWER(UNACCENT('areams')) || '%')) -AND tparty.active IS TRUE and (parent.active IS TRUE OR parent IS NULL)) -ORDER BY pertinence DESC, tparty.id ASC LIMIT 500 OFFSET 0; From 1409a3b23af53b9744cca5fd1dadc60874b82951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 13 Jul 2023 10:27:29 +0200 Subject: [PATCH 47/50] update changelog --- .changes/unreleased/Fixed-20230713-102640.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changes/unreleased/Fixed-20230713-102640.yaml diff --git a/.changes/unreleased/Fixed-20230713-102640.yaml b/.changes/unreleased/Fixed-20230713-102640.yaml new file mode 100644 index 000000000..e731e5252 --- /dev/null +++ b/.changes/unreleased/Fixed-20230713-102640.yaml @@ -0,0 +1,6 @@ +kind: Fixed +body: | + Correct bug in thirdparty API search query: simplify address joins clause for child and parent kind +time: 2023-07-13T10:26:40.503796155+02:00 +custom: + Issue: "126" From 28c41aaf8543dac4a3f3da0eb195c87199e19c97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 13 Jul 2023 10:32:56 +0200 Subject: [PATCH 48/50] fix the number of parameters in the query --- src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php b/src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php index bb5303143..1d4e12074 100644 --- a/src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php +++ b/src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php @@ -100,7 +100,7 @@ class ThirdPartyApiSearch implements SearchApiInterface ) + " . // take postcode label into account, but lower than the canonicalized field "COALESCE((LOWER(UNACCENT(cmpc.label)) LIKE '%' || LOWER(UNACCENT(?)) || '%')::int * 0.3, 0)"; - $pertinenceArgs[] = [$str, $str, $str, $str, $str, $str]; + $pertinenceArgs[] = [$str, $str, $str, $str, $str]; } } From c0901947ca3e8d3b7700157abf8034ea590ea169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 13 Jul 2023 17:36:05 +0200 Subject: [PATCH 49/50] [filterOrder] show selected picked users in the results boxes --- .../Listing/FilterOrderGetActiveFilterHelper.php | 8 ++++++++ .../Templating/Listing/FilterOrderHelper.php | 4 ++++ .../Templating/Listing/FilterOrderPositionEnum.php | 1 + 3 files changed, 13 insertions(+) diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderGetActiveFilterHelper.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderGetActiveFilterHelper.php index 6b204e552..5b36e52b3 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderGetActiveFilterHelper.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderGetActiveFilterHelper.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Templating\Listing; +use Chill\MainBundle\Templating\Entity\UserRender; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\PropertyAccess\PropertyPathInterface; use Symfony\Contracts\Translation\TranslatorInterface; @@ -20,6 +21,7 @@ final readonly class FilterOrderGetActiveFilterHelper public function __construct( private TranslatorInterface $translator, private PropertyAccessorInterface $propertyAccessor, + private UserRender $userRender, ) { } @@ -73,6 +75,12 @@ final readonly class FilterOrderGetActiveFilterHelper } } + foreach ($filterOrderHelper->getUserPickers() as $name => ['label' => $label, 'options' => $options]) { + foreach ($filterOrderHelper->getUserPickerData($name) as $user) { + $result[] = ['value' => $this->userRender->renderString($user, []), 'label' => (string) $label, 'position' => FilterOrderPositionEnum::UserPicker->value, 'name' => $name]; + } + } + foreach ($filterOrderHelper->getSingleCheckbox() as $name => ['label' => $label]) { if (true === $filterOrderHelper->getSingleCheckboxData($name)) { $result[] = ['label' => '', 'value' => $this->translator->trans($label), 'position' => FilterOrderPositionEnum::SingleCheckbox->value, 'name' => $name]; diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php index 801b700a8..74d9a7a22 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Templating\Listing; +use Chill\MainBundle\Entity\User; use Chill\MainBundle\Form\Type\Listing\FilterOrderType; use DateTimeImmutable; use Symfony\Component\Form\Extension\Core\Type\FormType; @@ -134,6 +135,9 @@ final class FilterOrderHelper return $this->userPickers; } + /** + * @return list + */ public function getUserPickerData(string $name): array { return $this->getFormData()['user_pickers'][$name]; diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderPositionEnum.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderPositionEnum.php index 09e8d39aa..ed2337f1d 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderPositionEnum.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderPositionEnum.php @@ -18,4 +18,5 @@ enum FilterOrderPositionEnum: string case DateRange = 'date_range'; case EntityChoice = 'entity_choice'; case SingleCheckbox = 'single_checkbox'; + case UserPicker = 'user_picker'; } From 872d5e8ebf99895f41d3ec80d6bc8c4fbf8ab3a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 13 Jul 2023 18:02:46 +0200 Subject: [PATCH 50/50] do not show filter by user on 'my tasks' page, and show different states dynamically --- .../Controller/SingleTaskController.php | 28 ++++++----- .../Repository/SingleTaskStateRepository.php | 47 +++++++++++++++++++ .../config/services/repositories.yaml | 10 ++-- 3 files changed, 71 insertions(+), 14 deletions(-) create mode 100644 src/Bundle/ChillTaskBundle/Repository/SingleTaskStateRepository.php diff --git a/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php b/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php index 22e49704f..3523c7a58 100644 --- a/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php +++ b/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php @@ -26,6 +26,7 @@ use Chill\TaskBundle\Event\TaskEvent; use Chill\TaskBundle\Event\UI\UIEvent; use Chill\TaskBundle\Form\SingleTaskType; use Chill\TaskBundle\Repository\SingleTaskAclAwareRepositoryInterface; +use Chill\TaskBundle\Repository\SingleTaskStateRepository; use Chill\TaskBundle\Security\Authorization\TaskVoter; use LogicException; use Psr\Log\LoggerInterface; @@ -71,7 +72,8 @@ final class SingleTaskController extends AbstractController EventDispatcherInterface $eventDispatcher, TimelineBuilder $timelineBuilder, LoggerInterface $logger, - FilterOrderHelperFactoryInterface $filterOrderHelperFactory + FilterOrderHelperFactoryInterface $filterOrderHelperFactory, + private SingleTaskStateRepository $singleTaskStateRepository ) { $this->eventDispatcher = $eventDispatcher; $this->timelineBuilder = $timelineBuilder; @@ -452,7 +454,7 @@ final class SingleTaskController extends AbstractController { $this->denyAccessUnlessGranted('ROLE_USER'); - $filterOrder = $this->buildFilterOrder(); + $filterOrder = $this->buildFilterOrder(false); $flags = array_merge( $filterOrder->getCheckboxData('status'), array_map(static fn ($i) => 'state_' . $i, $filterOrder->getCheckboxData('states')) @@ -667,7 +669,7 @@ final class SingleTaskController extends AbstractController return $form; } - private function buildFilterOrder(): FilterOrderHelper + private function buildFilterOrder($includeFilterByUser = true): FilterOrderHelper { $statuses = ['no-alert', 'warning', 'alert']; $statusTrans = [ @@ -675,18 +677,22 @@ final class SingleTaskController extends AbstractController 'Tasks near deadline', 'Tasks over deadline', ]; - $states = [ - // todo: get a list of possible states dynamically - 'new', 'in_progress', 'closed', 'canceled', - ]; + $states = $this->singleTaskStateRepository->findAllExistingStates(); + $checked = array_values(array_filter($states, fn (string $state) => !in_array($state, ['closed', 'canceled', 'validated'], true))); - return $this->filterOrderHelperFactory + $filterBuilder = $this->filterOrderHelperFactory ->create(self::class) ->addSearchBox() ->addCheckbox('status', $statuses, $statuses, $statusTrans) - ->addCheckbox('states', $states, ['new', 'in_progress']) - ->addUserPicker('userPicker', 'Filter by user', ['multiple' => true, 'required' => false]) - ->build(); + ->addCheckbox('states', $states, $checked) + ; + + if ($includeFilterByUser) { + $filterBuilder + ->addUserPicker('userPicker', 'Filter by user', ['multiple' => true, 'required' => false]); + } + + return $filterBuilder->build(); } /** diff --git a/src/Bundle/ChillTaskBundle/Repository/SingleTaskStateRepository.php b/src/Bundle/ChillTaskBundle/Repository/SingleTaskStateRepository.php new file mode 100644 index 000000000..2d64c69a0 --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Repository/SingleTaskStateRepository.php @@ -0,0 +1,47 @@ + + * @throws Exception + */ + public function findAllExistingStates(): array + { + $states = []; + + foreach ($this->connection->fetchAllNumeric(self::FIND_ALL_STATES) as $row) { + if ('' !== $row[0] && null !== $row[0]) { + $states[] = $row[0]; + } + } + + return $states; + } + +} diff --git a/src/Bundle/ChillTaskBundle/config/services/repositories.yaml b/src/Bundle/ChillTaskBundle/config/services/repositories.yaml index 7bee5abd0..9681e5d5c 100644 --- a/src/Bundle/ChillTaskBundle/config/services/repositories.yaml +++ b/src/Bundle/ChillTaskBundle/config/services/repositories.yaml @@ -1,4 +1,8 @@ services: + _defaults: + autowire: true + autoconfigure: true + chill_task.single_task_repository: class: Chill\TaskBundle\Repository\SingleTaskRepository factory: ['@doctrine.orm.entity_manager', getRepository] @@ -10,8 +14,8 @@ services: - "@chill.main.security.authorization.helper" Chill\TaskBundle\Repository\SingleTaskRepository: '@chill_task.single_task_repository' - Chill\TaskBundle\Repository\SingleTaskAclAwareRepository: - autowire: true - autoconfigure: true + Chill\TaskBundle\Repository\SingleTaskAclAwareRepository: ~ Chill\TaskBundle\Repository\SingleTaskAclAwareRepositoryInterface: '@Chill\TaskBundle\Repository\SingleTaskAclAwareRepository' + + Chill\TaskBundle\Repository\SingleTaskStateRepository: ~