From 00444e1e56fbe82806ad50a86be40f0313acafc8 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 20 Feb 2024 10:10:44 +0100 Subject: [PATCH 01/26] Add check on null value in template for closing motive --- .../Resources/views/AccompanyingCourse/index.html.twig | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/index.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/index.html.twig index f1dbf7e89..dce01a7ee 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/index.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/index.html.twig @@ -35,9 +35,11 @@

{{ 'This course is closed'|trans }}

-

- {{ 'Closing motive'|trans }} : {{ accompanyingCourse.closingMotive.name|localize_translatable_string }} -

+ {% if accompanyingCourse.closingMotive %} +

+ {{ 'Closing motive'|trans }} : {{ accompanyingCourse.closingMotive.name|localize_translatable_string }} +

+ {% endif %}
From 4a2078dc65f16effa57176c224090633c7c459ec Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 21 Feb 2024 19:49:43 +0100 Subject: [PATCH 02/26] upgrade to 2.16.2 --- .changes/v2.16.2.md | 3 +++ CHANGELOG.md | 4 ++++ 2 files changed, 7 insertions(+) create mode 100644 .changes/v2.16.2.md diff --git a/.changes/v2.16.2.md b/.changes/v2.16.2.md new file mode 100644 index 000000000..3aa01ff16 --- /dev/null +++ b/.changes/v2.16.2.md @@ -0,0 +1,3 @@ +## v2.16.2 - 2024-02-21 +### Fixed +* Check for null values in closing motive of parcours d'accompagnement for correct rendering of template diff --git a/CHANGELOG.md b/CHANGELOG.md index 3986a1e64..c993fb2ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). +## v2.16.2 - 2024-02-21 +### Fixed +* Check for null values in closing motive of parcours d'accompagnement for correct rendering of template + ## v2.16.1 - 2024-02-09 ### Fixed * Force bootstrap version to avoid error in builds with newer version From 97f2c75de89896a7b8e20d511e9e0fa89e5fcb69 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 21 Feb 2024 20:14:18 +0100 Subject: [PATCH 03/26] Change syntax of check on null for closing motive --- .../Resources/views/AccompanyingCourse/index.html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/index.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/index.html.twig index dce01a7ee..3ef4825ef 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/index.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/index.html.twig @@ -35,7 +35,7 @@

{{ 'This course is closed'|trans }}

- {% if accompanyingCourse.closingMotive %} + {% if accompanyingCourse.closingMotive is not same as null %}

{{ 'Closing motive'|trans }} : {{ accompanyingCourse.closingMotive.name|localize_translatable_string }}

From 569aeeef877a19a51d104b1fadf973203e4ed578 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 26 Feb 2024 12:23:11 +0100 Subject: [PATCH 04/26] =?UTF-8?q?Fix=20wrong=20translation=20of=20user=20j?= =?UTF-8?q?ob=20service=20->=20m=C3=A9tier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Bundle/ChillActivityBundle/translations/messages.fr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml index 6d447ea21..441c23eeb 100644 --- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml @@ -396,7 +396,7 @@ export: by_creator_job: job_form_label: Métiers Filter activity by user job: Filtrer les échanges par métier du créateur de l'échange - 'Filtered activity by user job: only %jobs%': "Filtré par service du créateur de l'échange: uniquement %jobs%" + 'Filtered activity by user job: only %jobs%': "Filtré par métier du créateur de l'échange: uniquement %jobs%" by_persons: Filter activity by persons: Filtrer les échanges par usager participant 'Filtered activity by persons: only %persons%': 'Échanges filtrés par usagers participants: seulement %persons%' From d713087dcbc8edb0d398f3c420d97f227f8c0e6a Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 26 Feb 2024 13:30:26 +0100 Subject: [PATCH 05/26] Changie and php style fixes --- .changes/unreleased/Fixed-20240226-122241.yaml | 5 +++++ src/Bundle/ChillActivityBundle/Form/ActivityType.php | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changes/unreleased/Fixed-20240226-122241.yaml diff --git a/.changes/unreleased/Fixed-20240226-122241.yaml b/.changes/unreleased/Fixed-20240226-122241.yaml new file mode 100644 index 000000000..f2a35ce1f --- /dev/null +++ b/.changes/unreleased/Fixed-20240226-122241.yaml @@ -0,0 +1,5 @@ +kind: Fixed +body: Fix translation of user job -> 'service' must be 'métier' +time: 2024-02-26T12:22:41.140939553+01:00 +custom: + Issue: "236" diff --git a/src/Bundle/ChillActivityBundle/Form/ActivityType.php b/src/Bundle/ChillActivityBundle/Form/ActivityType.php index d322089c8..fbcf60bf0 100644 --- a/src/Bundle/ChillActivityBundle/Form/ActivityType.php +++ b/src/Bundle/ChillActivityBundle/Form/ActivityType.php @@ -95,7 +95,7 @@ class ActivityType extends AbstractType ]); } - /** @var \Chill\PersonBundle\Entity\AccompanyingPeriod|null $accompanyingPeriod */ + /** @var AccompanyingPeriod|null $accompanyingPeriod */ $accompanyingPeriod = null; if ($options['accompanyingPeriod'] instanceof AccompanyingPeriod) { From bbb167bb85a531e0e08b03e7186eb5fdae97c3b9 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 26 Feb 2024 13:36:44 +0100 Subject: [PATCH 06/26] order user jobs alphabetically when returning all active user jobs --- .../ChillActivityBundle/Export/Filter/CreatorJobFilter.php | 2 +- .../ChillActivityBundle/Export/Filter/UsersJobFilter.php | 5 ++++- .../src/Export/Filter/ByUserJobFilter.php | 5 ++++- src/Bundle/ChillCalendarBundle/Export/Filter/JobFilter.php | 5 ++++- src/Bundle/ChillEventBundle/Controller/EventController.php | 1 + src/Bundle/ChillMainBundle/Repository/UserJobRepository.php | 6 +++++- .../AccompanyingCourseFilters/JobWorkingOnCourseFilter.php | 5 +---- .../Export/Filter/SocialWorkFilters/JobFilter.php | 5 ++++- 8 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/CreatorJobFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/CreatorJobFilter.php index 6ce340874..c43edb666 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/CreatorJobFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/CreatorJobFilter.php @@ -80,7 +80,7 @@ final readonly class CreatorJobFilter implements FilterInterface { $builder ->add('jobs', EntityType::class, [ - 'choices' => $this->userJobRepository->findAllOrderedByName(), + 'choices' => $this->userJobRepository->findAllActive(), 'class' => UserJob::class, 'choice_label' => fn (UserJob $s) => $this->translatableStringHelper->localize( $s->getLabel() diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/UsersJobFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/UsersJobFilter.php index ae21482fc..0987e7ae9 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/UsersJobFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/UsersJobFilter.php @@ -16,6 +16,7 @@ use Chill\ActivityBundle\Export\Declarations; use Chill\MainBundle\Entity\User\UserJobHistory; use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Repository\UserJobRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\QueryBuilder; @@ -27,7 +28,8 @@ class UsersJobFilter implements FilterInterface private const PREFIX = 'act_filter_user_job'; public function __construct( - private readonly TranslatableStringHelperInterface $translatableStringHelper + private readonly TranslatableStringHelperInterface $translatableStringHelper, + private readonly UserJobRepositoryInterface $userJobRepository ) { } @@ -69,6 +71,7 @@ class UsersJobFilter implements FilterInterface $builder ->add('jobs', EntityType::class, [ 'class' => UserJob::class, + 'choices' => $this->userJobRepository->findAllActive(), 'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()), 'multiple' => true, 'expanded' => true, diff --git a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserJobFilter.php b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserJobFilter.php index 2418f5428..691e708c6 100644 --- a/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserJobFilter.php +++ b/src/Bundle/ChillAsideActivityBundle/src/Export/Filter/ByUserJobFilter.php @@ -16,6 +16,7 @@ use Chill\AsideActivityBundle\Export\Declarations; use Chill\MainBundle\Entity\User\UserJobHistory; use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Repository\UserJobRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\QueryBuilder; @@ -27,7 +28,8 @@ class ByUserJobFilter implements FilterInterface private const PREFIX = 'aside_act_filter_user_job'; public function __construct( - private readonly TranslatableStringHelperInterface $translatableStringHelper + private readonly TranslatableStringHelperInterface $translatableStringHelper, + private readonly UserJobRepositoryInterface $userJobRepository ) { } @@ -69,6 +71,7 @@ class ByUserJobFilter implements FilterInterface $builder ->add('jobs', EntityType::class, [ 'class' => UserJob::class, + 'choices' => $this->userJobRepository->findAllActive(), 'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()), 'multiple' => true, 'expanded' => true, diff --git a/src/Bundle/ChillCalendarBundle/Export/Filter/JobFilter.php b/src/Bundle/ChillCalendarBundle/Export/Filter/JobFilter.php index 6b81a709f..b2b5e6a12 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Filter/JobFilter.php +++ b/src/Bundle/ChillCalendarBundle/Export/Filter/JobFilter.php @@ -15,6 +15,7 @@ use Chill\CalendarBundle\Export\Declarations; use Chill\MainBundle\Entity\User\UserJobHistory; use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Repository\UserJobRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelper; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; @@ -26,7 +27,8 @@ final readonly class JobFilter implements FilterInterface private const PREFIX = 'cal_filter_job'; public function __construct( - private TranslatableStringHelper $translatableStringHelper + private TranslatableStringHelper $translatableStringHelper, + private UserJobRepositoryInterface $userJobRepository ) { } @@ -74,6 +76,7 @@ final readonly class JobFilter implements FilterInterface $builder ->add('job', EntityType::class, [ 'class' => UserJob::class, + 'choices' => $this->userJobRepository->findAllActive(), 'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize( $j->getLabel() ), diff --git a/src/Bundle/ChillEventBundle/Controller/EventController.php b/src/Bundle/ChillEventBundle/Controller/EventController.php index 7cbd2fbbe..e49c02c06 100644 --- a/src/Bundle/ChillEventBundle/Controller/EventController.php +++ b/src/Bundle/ChillEventBundle/Controller/EventController.php @@ -433,6 +433,7 @@ final class EventController extends AbstractController $builder->add('event_id', HiddenType::class, [ 'data' => $event->getId(), ]); + dump($event->getId()); return $builder->getForm(); } diff --git a/src/Bundle/ChillMainBundle/Repository/UserJobRepository.php b/src/Bundle/ChillMainBundle/Repository/UserJobRepository.php index 3c92c7f60..6b793a2c8 100644 --- a/src/Bundle/ChillMainBundle/Repository/UserJobRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/UserJobRepository.php @@ -40,7 +40,11 @@ readonly class UserJobRepository implements UserJobRepositoryInterface public function findAllActive(): array { - return $this->repository->findBy(['active' => true]); + $jobs = $this->repository->findBy(['active' => true]); + + usort($jobs, fn (UserJob $a, UserJob $b) => $this->translatableStringHelper->localize($a->getLabel()) <=> $this->translatableStringHelper->localize($b->getLabel())); + + return $jobs; } public function findAllOrderedByName(): array diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/JobWorkingOnCourseFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/JobWorkingOnCourseFilter.php index 8ba1b585d..73d5a02c9 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/JobWorkingOnCourseFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/JobWorkingOnCourseFilter.php @@ -84,13 +84,10 @@ readonly class JobWorkingOnCourseFilter implements FilterInterface public function buildForm(FormBuilderInterface $builder): void { - $jobs = $this->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, + 'choices' => $this->userJobRepository->findAllActive(), 'choice_label' => fn (UserJob $userJob) => $this->translatableStringHelper->localize($userJob->getLabel()), 'multiple' => true, 'expanded' => true, diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/JobFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/JobFilter.php index 4622f61f9..bb54f288b 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/JobFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/JobFilter.php @@ -13,6 +13,7 @@ namespace Chill\PersonBundle\Export\Filter\SocialWorkFilters; use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Repository\UserJobRepositoryInterface; use Chill\MainBundle\Service\RollingDate\RollingDate; use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkReferrerHistory; @@ -28,7 +29,8 @@ class JobFilter implements FilterInterface public function __construct( protected TranslatorInterface $translator, - private readonly TranslatableStringHelper $translatableStringHelper + private readonly TranslatableStringHelper $translatableStringHelper, + private readonly UserJobRepositoryInterface $userJobRepository, ) { } @@ -61,6 +63,7 @@ class JobFilter implements FilterInterface $builder ->add('job', EntityType::class, [ 'class' => UserJob::class, + 'choices' => $this->userJobRepository->findAllActive(), 'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize( $j->getLabel() ), From 5b714f17be31a980fdd9445cc56977b77e28799e Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 26 Feb 2024 14:39:10 +0100 Subject: [PATCH 07/26] order scopes alphabetically --- .changes/unreleased/UX-20240226-143943.yaml | 5 +++++ .../Export/Filter/CreatorScopeFilter.php | 5 ++++- .../ChillCalendarBundle/Export/Filter/ScopeFilter.php | 5 ++++- .../ChillMainBundle/Repository/ScopeRepository.php | 9 +++++---- .../ScopeWorkingOnCourseFilter.php | 5 +---- .../Export/Filter/SocialWorkFilters/ScopeFilter.php | 5 ++++- 6 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 .changes/unreleased/UX-20240226-143943.yaml diff --git a/.changes/unreleased/UX-20240226-143943.yaml b/.changes/unreleased/UX-20240226-143943.yaml new file mode 100644 index 000000000..84c7ffb44 --- /dev/null +++ b/.changes/unreleased/UX-20240226-143943.yaml @@ -0,0 +1,5 @@ +kind: UX +body: Order user jobs and services alphabetically in export filters +time: 2024-02-26T14:39:43.283296338+01:00 +custom: + Issue: "232" diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/CreatorScopeFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/CreatorScopeFilter.php index a6ab07ade..dad670775 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/CreatorScopeFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/CreatorScopeFilter.php @@ -15,6 +15,7 @@ use Chill\ActivityBundle\Export\Declarations; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User\UserScopeHistory; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Repository\ScopeRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelper; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; @@ -26,7 +27,8 @@ class CreatorScopeFilter implements FilterInterface private const PREFIX = 'acp_act_filter_creator_scope'; public function __construct( - private readonly TranslatableStringHelper $translatableStringHelper + private readonly TranslatableStringHelper $translatableStringHelper, + private readonly ScopeRepositoryInterface $scopeRepository, ) { } @@ -76,6 +78,7 @@ class CreatorScopeFilter implements FilterInterface $builder ->add('scopes', EntityType::class, [ 'class' => Scope::class, + 'choices' => $this->scopeRepository->findAllActive(), 'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize( $s->getName() ), diff --git a/src/Bundle/ChillCalendarBundle/Export/Filter/ScopeFilter.php b/src/Bundle/ChillCalendarBundle/Export/Filter/ScopeFilter.php index ef7c14199..179258e41 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Filter/ScopeFilter.php +++ b/src/Bundle/ChillCalendarBundle/Export/Filter/ScopeFilter.php @@ -15,6 +15,7 @@ use Chill\CalendarBundle\Export\Declarations; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User\UserScopeHistory; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Repository\ScopeRepositoryInterface; use Chill\MainBundle\Templating\TranslatableStringHelper; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; @@ -28,7 +29,8 @@ class ScopeFilter implements FilterInterface public function __construct( protected TranslatorInterface $translator, - private readonly TranslatableStringHelper $translatableStringHelper + private readonly TranslatableStringHelper $translatableStringHelper, + private readonly ScopeRepositoryInterface $scopeRepository ) { } @@ -76,6 +78,7 @@ class ScopeFilter implements FilterInterface $builder ->add('scope', EntityType::class, [ 'class' => Scope::class, + 'choices' => $this->scopeRepository->findAllActive(), 'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize( $s->getName() ), diff --git a/src/Bundle/ChillMainBundle/Repository/ScopeRepository.php b/src/Bundle/ChillMainBundle/Repository/ScopeRepository.php index ba4ddae9b..aa3c78270 100644 --- a/src/Bundle/ChillMainBundle/Repository/ScopeRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/ScopeRepository.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Repository; use Chill\MainBundle\Entity\Scope; +use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\QueryBuilder; @@ -20,7 +21,7 @@ final class ScopeRepository implements ScopeRepositoryInterface { private readonly EntityRepository $repository; - public function __construct(EntityManagerInterface $entityManager) + public function __construct(EntityManagerInterface $entityManager, private readonly TranslatableStringHelperInterface $translatableStringHelper) { $this->repository = $entityManager->getRepository(Scope::class); } @@ -45,11 +46,11 @@ final class ScopeRepository implements ScopeRepositoryInterface public function findAllActive(): array { - $qb = $this->repository->createQueryBuilder('s'); + $scopes = $this->repository->findBy(['active' => true]); - $qb->where('s.active = \'TRUE\''); + usort($scopes, fn (Scope $a, Scope $b) => $this->translatableStringHelper->localize($a->getName()) <=> $this->translatableStringHelper->localize($b->getName())); - return $qb->getQuery()->getResult(); + return $scopes; } /** diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ScopeWorkingOnCourseFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ScopeWorkingOnCourseFilter.php index 9aec79c94..554023f1a 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ScopeWorkingOnCourseFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ScopeWorkingOnCourseFilter.php @@ -84,13 +84,10 @@ readonly class ScopeWorkingOnCourseFilter implements FilterInterface public function buildForm(FormBuilderInterface $builder): void { - $scopes = $this->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, + 'choices' => $this->scopeRepository->findAllActive(), 'choice_label' => fn (Scope $scope) => $this->translatableStringHelper->localize($scope->getName()), 'multiple' => true, 'expanded' => true, diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ScopeFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ScopeFilter.php index 2569c3d5b..9a0847cfa 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ScopeFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ScopeFilter.php @@ -13,6 +13,7 @@ namespace Chill\PersonBundle\Export\Filter\SocialWorkFilters; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Export\FilterInterface; +use Chill\MainBundle\Repository\ScopeRepositoryInterface; use Chill\MainBundle\Service\RollingDate\RollingDate; use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkReferrerHistory; @@ -28,7 +29,8 @@ class ScopeFilter implements FilterInterface public function __construct( protected TranslatorInterface $translator, - private readonly TranslatableStringHelper $translatableStringHelper + private readonly TranslatableStringHelper $translatableStringHelper, + private readonly ScopeRepositoryInterface $scopeRepository ) { } @@ -60,6 +62,7 @@ class ScopeFilter implements FilterInterface $builder ->add('scope', EntityType::class, [ 'class' => Scope::class, + 'choices' => $this->scopeRepository->findAllActive(), 'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize( $s->getName() ), From c888b5b84f52e79cade0eeecd631418eb759f750 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 26 Feb 2024 14:53:20 +0100 Subject: [PATCH 08/26] Update chill bundles version to 2.16.3 --- .changes/unreleased/Fixed-20240226-122241.yaml | 5 ----- .changes/unreleased/UX-20240226-143943.yaml | 5 ----- .changes/v2.16.3.md | 5 +++++ CHANGELOG.md | 6 ++++++ 4 files changed, 11 insertions(+), 10 deletions(-) delete mode 100644 .changes/unreleased/Fixed-20240226-122241.yaml delete mode 100644 .changes/unreleased/UX-20240226-143943.yaml create mode 100644 .changes/v2.16.3.md diff --git a/.changes/unreleased/Fixed-20240226-122241.yaml b/.changes/unreleased/Fixed-20240226-122241.yaml deleted file mode 100644 index f2a35ce1f..000000000 --- a/.changes/unreleased/Fixed-20240226-122241.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Fixed -body: Fix translation of user job -> 'service' must be 'métier' -time: 2024-02-26T12:22:41.140939553+01:00 -custom: - Issue: "236" diff --git a/.changes/unreleased/UX-20240226-143943.yaml b/.changes/unreleased/UX-20240226-143943.yaml deleted file mode 100644 index 84c7ffb44..000000000 --- a/.changes/unreleased/UX-20240226-143943.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: UX -body: Order user jobs and services alphabetically in export filters -time: 2024-02-26T14:39:43.283296338+01:00 -custom: - Issue: "232" diff --git a/.changes/v2.16.3.md b/.changes/v2.16.3.md new file mode 100644 index 000000000..7bb143382 --- /dev/null +++ b/.changes/v2.16.3.md @@ -0,0 +1,5 @@ +## v2.16.3 - 2024-02-26 +### Fixed +* ([#236](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/236)) Fix translation of user job -> 'service' must be 'métier' +### UX +* ([#232](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/232)) Order user jobs and services alphabetically in export filters diff --git a/CHANGELOG.md b/CHANGELOG.md index c993fb2ee..8f5d91ddc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). +## v2.16.3 - 2024-02-26 +### Fixed +* ([#236](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/236)) Fix translation of user job -> 'service' must be 'métier' +### UX +* ([#232](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/232)) Order user jobs and services alphabetically in export filters + ## v2.16.2 - 2024-02-21 ### Fixed * Check for null values in closing motive of parcours d'accompagnement for correct rendering of template From 09578a775c95d746763d058ed679b13802a2cee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 6 Mar 2024 12:31:35 +0100 Subject: [PATCH 09/26] Update documentation to explain use of EXISTS in SQL queries Added an explanatory section to the "exports.rst" doc to clarify why to use an EXISTS subquery instead of a JOIN clause in SQL queries involving many-to-* relationships. This explanation includes sample SQL queries and results to illustrate the potential issue of duplicates with JOIN and count, and how EXISTS can help avoid this issue. Also updated the ".editorconfig" file for .rst files. --- .editorconfig | 4 + docs/source/development/exports.rst | 126 ++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) diff --git a/.editorconfig b/.editorconfig index a3e5a0fc1..bede621e3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -23,3 +23,7 @@ max_line_length = 0 indent_size = 2 indent_style = space +[.rst] +ident_size = 3 +ident_style = space + diff --git a/docs/source/development/exports.rst b/docs/source/development/exports.rst index 3b01f9e0f..9447a0575 100644 --- a/docs/source/development/exports.rst +++ b/docs/source/development/exports.rst @@ -242,3 +242,129 @@ This is an example of the *filter by birthdate*. This filter asks some informati Continue to explain the export framework .. _main bundle: https://git.framasoft.org/Chill-project/Chill-Main + + +With many-to-* relationship, why should we set WHERE clauses in an EXISTS subquery instead of a JOIN ? +`````````````````````````````````````````````````````````````````````````````````````````````````````` + +As we described above, the doctrine builder is converted into a sql query. Let's see how to compute the "number of course +which count at least one activity type with the id 7". For the purpose of this demonstration, we will restrict this on +two accompanying period only: the ones with id 329 and 334. + +Let's see the list of activities associated with those accompanying period: + +.. code-block:: sql + + SELECT id, accompanyingperiod_id, type_id FROM activity WHERE accompanyingperiod_id IN (329, 334) AND type_id = 7 + ORDER BY accompanyingperiod_id; + +We see that we have 6 activities for the accompanying period with id 329, and only one for the 334's one. + +.. csv-table:: + :header: id, accompany$iungperiod_id, type_id + + 990,329,7 + 986,329,7 + 987,329,7 + 993,329,7 + 991,329,7 + 992,329,7 + 1000,334,7 + +Let's calculate the average duration for those accompanying periods, and the number of period: + +.. code-block:: sql + + SELECT AVG(age(COALESCE(closingdate, CURRENT_DATE), openingdate)), COUNT(id) from chill_person_accompanying_period WHERE id IN (329, 334); + +The result of this query is: + +.. csv-table:: + :header: AVG, COUNT + + 2 years 2 mons 21 days 12 hours 0 mins 0.0 secs,2 + +Now, we count the number of accompanying period, adding a :code:`JOIN` clause which make a link to the :code:`activity` table, and add a :code:`WHERE` clause to keep +only the accompanying period which contains the given activity type: + +.. code-block:: sql + + SELECT COUNT(chill_person_accompanying_period.id) from chill_person_accompanying_period + JOIN activity ON chill_person_accompanying_period.id = activity.accompanyingperiod_id + WHERE chill_person_accompanying_period.id IN (329, 334) AND activity.type_id = 7; + +What are the results here ? + +.. csv-table:: + :header: COUNT + + 7 + +:code:`7` ! Why this result ? Because the number of lines is duplicated for each activity. Let's see the list of rows which +are taken into account for the computation: + +.. code-block:: sql + + SELECT chill_person_accompanying_period.id, activity.id from chill_person_accompanying_period + JOIN activity ON chill_person_accompanying_period.id = activity.accompanyingperiod_id + WHERE chill_person_accompanying_period.id IN (329, 334) AND activity.type_id = 7; + +.. csv-table:: + :header: accompanyingperiod.id, activity.id + + 329,993 + 334,1000 + 329,987 + 329,990 + 329,991 + 329,992 + 329,986 + +For each activity, a row is created and, as we count the number of non-null :code:`accompanyingperiod.id` columns, we +count one entry for each activity (actually, we count the number of activities). + +So, let's use the :code:`DISTINCT` keyword to count only once the equal ids: + +.. code-block:: + + SELECT COUNT(DISTINCT chill_person_accompanying_period.id) from chill_person_accompanying_period + JOIN activity ON chill_person_accompanying_period.id = activity.accompanyingperiod_id + WHERE chill_person_accompanying_period.id IN (329, 334) AND activity.type_id = 7; + +Now, it works again... + +.. csv-table:: + :header: COUNT + + 2 + +But, for the average duration, this won't work: the duration which are equals (because the :code:`openingdate` is the same and +:code:`closingdate` is still :code:`NULL`, for instance) will be counted only once, which will give unexpected result. + +The solution is to move the condition "having an activity with activity type with id 7" in a :code:`EXISTS` clause: + +.. code-block:: sql + + SELECT COUNT(chill_person_accompanying_period.id) from chill_person_accompanying_period + WHERE chill_person_accompanying_period.id IN (329, 334) AND EXISTS (SELECT 1 FROM activity WHERE type_id = 7 AND accompanyingperiod_id = chill_person_accompanying_period.id); + +The result is correct without :code:`DISTINCT` keyword: + +.. csv-table:: + :header: COUNT + + 2 + +And we can now compute the average duration without fear: + +.. code-block:: sql + + SELECT AVG(age(COALESCE(closingdate, CURRENT_DATE), openingdate)) from chill_person_accompanying_period + WHERE chill_person_accompanying_period.id IN (329, 334) AND EXISTS (SELECT 1 FROM activity WHERE type_id = 7 AND accompanyingperiod_id = chill_person_accompanying_period.id); + +Give the result: + +.. csv-table:: + :header: AVG + + 2 years 2 mons 21 days 12 hours 0 mins 0.0 secs From f0dbb1717229d3d5d41ef0fa6786653b408a3e55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 6 Mar 2024 11:40:19 +0000 Subject: [PATCH 10/26] Update exports.rst: fix typo --- docs/source/development/exports.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/development/exports.rst b/docs/source/development/exports.rst index 9447a0575..7cb04f32e 100644 --- a/docs/source/development/exports.rst +++ b/docs/source/development/exports.rst @@ -261,7 +261,7 @@ Let's see the list of activities associated with those accompanying period: We see that we have 6 activities for the accompanying period with id 329, and only one for the 334's one. .. csv-table:: - :header: id, accompany$iungperiod_id, type_id + :header: id, accompanyingperiod_id, type_id 990,329,7 986,329,7 From 1d636f5e9ef9697e53c69e12ba4d0f3525ab85d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 7 Mar 2024 15:26:58 +0100 Subject: [PATCH 11/26] Fix deprecations and code style issues --- .../Connector/MSGraph/MSUserAbsenceReader.php | 2 +- .../MSGraph/MSUserAbsenceReaderInterface.php | 2 +- src/Bundle/ChillEventBundle/Entity/Event.php | 2 +- .../ChillEventBundle/Entity/Participation.php | 6 +++--- .../ChillMainBundle/Cron/CronJobInterface.php | 2 +- src/Bundle/ChillMainBundle/Doctrine/DQL/Age.php | 9 ++++----- .../ChillMainBundle/Doctrine/DQL/Extract.php | 11 +++++------ .../Doctrine/DQL/GetJsonFieldByKey.php | 9 ++++----- .../ChillMainBundle/Doctrine/DQL/Greatest.php | 11 +++++------ .../Doctrine/DQL/JsonAggregate.php | 7 +++---- .../Doctrine/DQL/JsonBuildObject.php | 11 +++++------ .../Doctrine/DQL/JsonExtract.php | 9 ++++----- .../Doctrine/DQL/JsonbArrayLength.php | 7 +++---- .../Doctrine/DQL/JsonbExistsInArray.php | 9 ++++----- .../ChillMainBundle/Doctrine/DQL/Least.php | 11 +++++------ .../ChillMainBundle/Doctrine/DQL/OverlapsI.php | 17 ++++++++--------- .../ChillMainBundle/Doctrine/DQL/Replace.php | 11 +++++------ .../ChillMainBundle/Doctrine/DQL/STContains.php | 9 ++++----- src/Bundle/ChillMainBundle/Doctrine/DQL/STX.php | 7 +++---- src/Bundle/ChillMainBundle/Doctrine/DQL/STY.php | 7 +++---- .../ChillMainBundle/Doctrine/DQL/Similarity.php | 9 ++++----- .../Doctrine/DQL/StrictWordSimilarityOPS.php | 9 ++++----- .../ChillMainBundle/Doctrine/DQL/ToChar.php | 9 ++++----- .../ChillMainBundle/Doctrine/DQL/Unaccent.php | 7 +++---- src/Bundle/ChillMainBundle/Entity/User.php | 4 ++-- ...eAddressWithReferenceOrPostalCodeCronJob.php | 2 +- ...oGeographicalUnitMaterializedViewCronJob.php | 2 +- .../Cron/CronJobDatabaseInteractionTest.php | 2 +- .../Tests/Cron/CronManagerTest.php | 4 ++-- .../Normalizer/UserNormalizerTest.php | 2 +- .../AccompanyingPeriodStepChangeCronjob.php | 2 +- .../Doctrine/DQL/AddressPart.php | 9 ++++----- .../Entity/AccompanyingPeriod.php | 4 ++-- src/Bundle/ChillPersonBundle/Entity/Person.php | 4 ++-- .../ClosingDateAggregator.php | 2 +- .../OpeningDateAggregator.php | 2 +- ...AccompanyingPeriodACLAwareRepositoryTest.php | 2 +- .../ChillTaskBundle/Entity/SingleTask.php | 8 ++++---- 38 files changed, 111 insertions(+), 131 deletions(-) diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php index 58cd04dfa..22dbaa2da 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php @@ -33,7 +33,7 @@ final readonly class MSUserAbsenceReader implements MSUserAbsenceReaderInterface /** * @throw UserAbsenceSyncException when the data cannot be reached or is not valid from microsoft */ - public function isUserAbsent(User $user): bool|null + public function isUserAbsent(User $user): ?bool { $id = $this->mapCalendarToUser->getUserId($user); diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderInterface.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderInterface.php index a918bb7ea..7c3fd69d6 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderInterface.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderInterface.php @@ -18,5 +18,5 @@ interface MSUserAbsenceReaderInterface /** * @throw UserAbsenceSyncException when the data cannot be reached or is not valid from microsoft */ - public function isUserAbsent(User $user): bool|null; + public function isUserAbsent(User $user): ?bool; } diff --git a/src/Bundle/ChillEventBundle/Entity/Event.php b/src/Bundle/ChillEventBundle/Entity/Event.php index 44f716edf..486a67c5c 100644 --- a/src/Bundle/ChillEventBundle/Entity/Event.php +++ b/src/Bundle/ChillEventBundle/Entity/Event.php @@ -197,7 +197,7 @@ class Event implements HasCenterInterface, HasScopeInterface, TrackCreationInter return $this->id; } - public function getModerator(): User|null + public function getModerator(): ?User { return $this->moderator; } diff --git a/src/Bundle/ChillEventBundle/Entity/Participation.php b/src/Bundle/ChillEventBundle/Entity/Participation.php index ded8ae8da..9b4664f0d 100644 --- a/src/Bundle/ChillEventBundle/Entity/Participation.php +++ b/src/Bundle/ChillEventBundle/Entity/Participation.php @@ -91,7 +91,7 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa /** * Get event. */ - public function getEvent(): Event|null + public function getEvent(): ?Event { return $this->event; } @@ -127,7 +127,7 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa /** * Get role. */ - public function getRole(): Role|null + public function getRole(): ?Role { return $this->role; } @@ -147,7 +147,7 @@ class Participation implements \ArrayAccess, HasCenterInterface, HasScopeInterfa /** * Get status. */ - public function getStatus(): Status|null + public function getStatus(): ?Status { return $this->status; } diff --git a/src/Bundle/ChillMainBundle/Cron/CronJobInterface.php b/src/Bundle/ChillMainBundle/Cron/CronJobInterface.php index dcc19f01c..050b777e9 100644 --- a/src/Bundle/ChillMainBundle/Cron/CronJobInterface.php +++ b/src/Bundle/ChillMainBundle/Cron/CronJobInterface.php @@ -28,5 +28,5 @@ interface CronJobInterface * * @return array|null optionally return an array with the same data than the previous execution */ - public function run(array $lastExecutionData): array|null; + public function run(array $lastExecutionData): ?array; } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/Age.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/Age.php index dd28a8efe..cce2f9ba4 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/Age.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/Age.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -40,15 +39,15 @@ class Age extends FunctionNode public function parse(Parser $parser) { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->value1 = $parser->SimpleArithmeticExpression(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->value2 = $parser->SimpleArithmeticExpression(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/Extract.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/Extract.php index 93000be9c..bc6134c60 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/Extract.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/Extract.php @@ -14,7 +14,6 @@ namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\DateDiffFunction; use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\PathExpression; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -45,17 +44,17 @@ class Extract extends FunctionNode public function parse(Parser $parser) { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); - $parser->match(Lexer::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); $this->field = $parser->getLexer()->token['value']; - $parser->match(Lexer::T_FROM); + $parser->match(\Doctrine\ORM\Query\TokenType::T_FROM); // $this->value = $parser->ScalarExpression(); $this->value = $parser->ArithmeticPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/GetJsonFieldByKey.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/GetJsonFieldByKey.php index 94d2da2e8..56b2a9cda 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/GetJsonFieldByKey.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/GetJsonFieldByKey.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -33,11 +32,11 @@ class GetJsonFieldByKey extends FunctionNode public function parse(Parser $parser) { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->expr1 = $parser->StringPrimary(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->expr2 = $parser->StringPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/Greatest.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/Greatest.php index 924831eb1..6467b93c5 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/Greatest.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/Greatest.php @@ -13,7 +13,6 @@ namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Node; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -41,15 +40,15 @@ class Greatest extends FunctionNode $this->exprs = []; $lexer = $parser->getLexer(); - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->exprs[] = $parser->ArithmeticPrimary(); - while (Lexer::T_COMMA === $lexer->lookahead['type']) { - $parser->match(Lexer::T_COMMA); + while (\Doctrine\ORM\Query\TokenType::T_COMMA === $lexer->lookahead['type']) { + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->exprs[] = $parser->ArithmeticPrimary(); } - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonAggregate.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonAggregate.php index 03809572d..171b069d3 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonAggregate.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonAggregate.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -33,9 +32,9 @@ class JsonAggregate extends FunctionNode public function parse(Parser $parser) { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->expr = $parser->StringPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonBuildObject.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonBuildObject.php index 675935867..ea6ea4c53 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonBuildObject.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonBuildObject.php @@ -13,7 +13,6 @@ namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Node; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -38,16 +37,16 @@ class JsonBuildObject extends FunctionNode public function parse(Parser $parser) { $lexer = $parser->getLexer(); - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->exprs[] = $parser->ArithmeticPrimary(); - while (Lexer::T_COMMA === $lexer->lookahead['type']) { - $parser->match(Lexer::T_COMMA); + while (\Doctrine\ORM\Query\TokenType::T_COMMA === $lexer->lookahead['type']) { + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->exprs[] = $parser->ArithmeticPrimary(); } - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonExtract.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonExtract.php index 95d851790..7abf15972 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonExtract.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonExtract.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -29,15 +28,15 @@ class JsonExtract extends FunctionNode public function parse(Parser $parser) { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->element = $parser->ArithmeticPrimary(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->keyToExtract = $parser->ArithmeticExpression(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonbArrayLength.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonbArrayLength.php index 4b3b75dfd..84f991fe8 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonbArrayLength.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonbArrayLength.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -33,9 +32,9 @@ class JsonbArrayLength extends FunctionNode public function parse(Parser $parser): void { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->expr1 = $parser->StringPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonbExistsInArray.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonbExistsInArray.php index 6ca3da89c..4c6d901d5 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonbExistsInArray.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonbExistsInArray.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -33,11 +32,11 @@ class JsonbExistsInArray extends FunctionNode public function parse(Parser $parser): void { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->expr1 = $parser->StringPrimary(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->expr2 = $parser->InputParameter(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/Least.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/Least.php index 7497e042e..aa6844e88 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/Least.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/Least.php @@ -13,7 +13,6 @@ namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\Node; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -41,15 +40,15 @@ class Least extends FunctionNode $this->exprs = []; $lexer = $parser->getLexer(); - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->exprs[] = $parser->ArithmeticPrimary(); - while (Lexer::T_COMMA === $lexer->lookahead['type']) { - $parser->match(Lexer::T_COMMA); + while (\Doctrine\ORM\Query\TokenType::T_COMMA === $lexer->lookahead['type']) { + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->exprs[] = $parser->ArithmeticPrimary(); } - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/OverlapsI.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/OverlapsI.php index 2495199b1..edf00243f 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/OverlapsI.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/OverlapsI.php @@ -13,7 +13,6 @@ namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\AST\PathExpression; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; /** @@ -45,29 +44,29 @@ class OverlapsI extends FunctionNode public function parse(Parser $parser): void { - $parser->match(Lexer::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->firstPeriodStart = $parser->StringPrimary(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->firstPeriodEnd = $parser->StringPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->secondPeriodStart = $parser->StringPrimary(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->secondPeriodEnd = $parser->StringPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } protected function makeCase($sqlWalker, $part, string $position): string diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/Replace.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/Replace.php index 7caf2c62c..d4c317f39 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/Replace.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/Replace.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; class Replace extends FunctionNode { @@ -35,19 +34,19 @@ class Replace extends FunctionNode public function parse(\Doctrine\ORM\Query\Parser $parser): void { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->string = $parser->StringPrimary(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->from = $parser->StringPrimary(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->to = $parser->StringPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/STContains.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/STContains.php index aa9bd29a4..9f3d1b861 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/STContains.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/STContains.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; /** * Geometry function 'ST_CONTAINS', added by postgis. @@ -31,15 +30,15 @@ class STContains extends FunctionNode public function parse(\Doctrine\ORM\Query\Parser $parser) { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->firstPart = $parser->StringPrimary(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->secondPart = $parser->StringPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/STX.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/STX.php index 2841bb729..f0d99d837 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/STX.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/STX.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -27,11 +26,11 @@ class STX extends FunctionNode public function parse(Parser $parser) { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->field = $parser->ArithmeticExpression(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/STY.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/STY.php index 9457a9357..42842605d 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/STY.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/STY.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -27,11 +26,11 @@ class STY extends FunctionNode public function parse(Parser $parser) { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->field = $parser->ArithmeticExpression(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/Similarity.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/Similarity.php index 2aa97fb9c..477e0c542 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/Similarity.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/Similarity.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; class Similarity extends FunctionNode { @@ -28,15 +27,15 @@ class Similarity extends FunctionNode public function parse(\Doctrine\ORM\Query\Parser $parser) { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->firstPart = $parser->StringPrimary(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->secondPart = $parser->StringPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/StrictWordSimilarityOPS.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/StrictWordSimilarityOPS.php index 7d0ef2acf..0096825a4 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/StrictWordSimilarityOPS.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/StrictWordSimilarityOPS.php @@ -11,7 +11,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -29,15 +28,15 @@ class StrictWordSimilarityOPS extends \Doctrine\ORM\Query\AST\Functions\Function public function parse(Parser $parser) { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->firstPart = $parser->StringPrimary(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->secondPart = $parser->StringPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/ToChar.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/ToChar.php index ef150867e..dc73aea92 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/ToChar.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/ToChar.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -36,11 +35,11 @@ class ToChar extends FunctionNode public function parse(Parser $parser) { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->datetime = $parser->ArithmeticExpression(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); $this->fmt = $parser->StringExpression(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/Unaccent.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/Unaccent.php index 5813c546f..12a745a0c 100644 --- a/src/Bundle/ChillMainBundle/Doctrine/DQL/Unaccent.php +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/Unaccent.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; /** * Unaccent string using postgresql extension unaccent : @@ -31,11 +30,11 @@ class Unaccent extends FunctionNode public function parse(\Doctrine\ORM\Query\Parser $parser) { - $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); $this->string = $parser->StringPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillMainBundle/Entity/User.php b/src/Bundle/ChillMainBundle/Entity/User.php index d5821c08f..05112430c 100644 --- a/src/Bundle/ChillMainBundle/Entity/User.php +++ b/src/Bundle/ChillMainBundle/Entity/User.php @@ -549,7 +549,7 @@ class User implements UserInterface, \Stringable $this->scopeHistories[] = $newScope; $criteria = new Criteria(); - $criteria->orderBy(['startDate' => Criteria::ASC, 'id' => Criteria::ASC]); + $criteria->orderBy(['startDate' => \Doctrine\Common\Collections\Order::Ascending, 'id' => \Doctrine\Common\Collections\Order::Ascending]); /** @var \Iterator $scopes */ $scopes = $this->scopeHistories->matching($criteria)->getIterator(); @@ -605,7 +605,7 @@ class User implements UserInterface, \Stringable $this->jobHistories[] = $newJob; $criteria = new Criteria(); - $criteria->orderBy(['startDate' => Criteria::ASC, 'id' => Criteria::ASC]); + $criteria->orderBy(['startDate' => \Doctrine\Common\Collections\Order::Ascending, 'id' => \Doctrine\Common\Collections\Order::Ascending]); /** @var \Iterator $jobs */ $jobs = $this->jobHistories->matching($criteria)->getIterator(); diff --git a/src/Bundle/ChillMainBundle/Service/AddressGeographicalUnit/CollateAddressWithReferenceOrPostalCodeCronJob.php b/src/Bundle/ChillMainBundle/Service/AddressGeographicalUnit/CollateAddressWithReferenceOrPostalCodeCronJob.php index 0267b126d..7c41db3a0 100644 --- a/src/Bundle/ChillMainBundle/Service/AddressGeographicalUnit/CollateAddressWithReferenceOrPostalCodeCronJob.php +++ b/src/Bundle/ChillMainBundle/Service/AddressGeographicalUnit/CollateAddressWithReferenceOrPostalCodeCronJob.php @@ -41,7 +41,7 @@ final readonly class CollateAddressWithReferenceOrPostalCodeCronJob implements C return 'collate-address'; } - public function run(array $lastExecutionData): array|null + public function run(array $lastExecutionData): ?array { $maxId = ($this->collateAddressWithReferenceOrPostalCode)($lastExecutionData[self::LAST_MAX_ID] ?? 0); diff --git a/src/Bundle/ChillMainBundle/Service/AddressGeographicalUnit/RefreshAddressToGeographicalUnitMaterializedViewCronJob.php b/src/Bundle/ChillMainBundle/Service/AddressGeographicalUnit/RefreshAddressToGeographicalUnitMaterializedViewCronJob.php index 46c79a250..77ce3a0c5 100644 --- a/src/Bundle/ChillMainBundle/Service/AddressGeographicalUnit/RefreshAddressToGeographicalUnitMaterializedViewCronJob.php +++ b/src/Bundle/ChillMainBundle/Service/AddressGeographicalUnit/RefreshAddressToGeographicalUnitMaterializedViewCronJob.php @@ -46,7 +46,7 @@ final readonly class RefreshAddressToGeographicalUnitMaterializedViewCronJob imp return 'refresh-materialized-view-address-to-geog-units'; } - public function run(array $lastExecutionData): array|null + public function run(array $lastExecutionData): ?array { $this->connection->executeQuery('REFRESH MATERIALIZED VIEW view_chill_main_address_geographical_unit'); diff --git a/src/Bundle/ChillMainBundle/Tests/Cron/CronJobDatabaseInteractionTest.php b/src/Bundle/ChillMainBundle/Tests/Cron/CronJobDatabaseInteractionTest.php index 38e2c9509..50b0f5584 100644 --- a/src/Bundle/ChillMainBundle/Tests/Cron/CronJobDatabaseInteractionTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Cron/CronJobDatabaseInteractionTest.php @@ -91,7 +91,7 @@ class JobWithReturn implements CronJobInterface return 'with-data'; } - public function run(array $lastExecutionData): array|null + public function run(array $lastExecutionData): ?array { return ['data' => 'test']; } diff --git a/src/Bundle/ChillMainBundle/Tests/Cron/CronManagerTest.php b/src/Bundle/ChillMainBundle/Tests/Cron/CronManagerTest.php index 417e1117b..80131a8e8 100644 --- a/src/Bundle/ChillMainBundle/Tests/Cron/CronManagerTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Cron/CronManagerTest.php @@ -175,7 +175,7 @@ class JobCanRun implements CronJobInterface return $this->key; } - public function run(array $lastExecutionData): array|null + public function run(array $lastExecutionData): ?array { return null; } @@ -193,7 +193,7 @@ class JobCannotRun implements CronJobInterface return 'job-b'; } - public function run(array $lastExecutionData): array|null + public function run(array $lastExecutionData): ?array { return null; } diff --git a/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/UserNormalizerTest.php b/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/UserNormalizerTest.php index 15334298e..96587a24c 100644 --- a/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/UserNormalizerTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Serializer/Normalizer/UserNormalizerTest.php @@ -117,7 +117,7 @@ final class UserNormalizerTest extends TestCase * * @throws ExceptionInterface */ - public function testNormalize(User|null $user, mixed $format, mixed $context, mixed $expected) + public function testNormalize(?User $user, mixed $format, mixed $context, mixed $expected) { $userRender = $this->prophesize(UserRender::class); $userRender->renderString(Argument::type(User::class), Argument::type('array'))->willReturn($user ? $user->getLabel() : ''); diff --git a/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Lifecycle/AccompanyingPeriodStepChangeCronjob.php b/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Lifecycle/AccompanyingPeriodStepChangeCronjob.php index 33d88e7de..73030f42c 100644 --- a/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Lifecycle/AccompanyingPeriodStepChangeCronjob.php +++ b/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Lifecycle/AccompanyingPeriodStepChangeCronjob.php @@ -39,7 +39,7 @@ readonly class AccompanyingPeriodStepChangeCronjob implements CronJobInterface return 'accompanying-period-step-change'; } - public function run(array $lastExecutionData): array|null + public function run(array $lastExecutionData): ?array { ($this->requestor)(); diff --git a/src/Bundle/ChillPersonBundle/Doctrine/DQL/AddressPart.php b/src/Bundle/ChillPersonBundle/Doctrine/DQL/AddressPart.php index 229a69f2c..2ebc1a859 100644 --- a/src/Bundle/ChillPersonBundle/Doctrine/DQL/AddressPart.php +++ b/src/Bundle/ChillPersonBundle/Doctrine/DQL/AddressPart.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\PersonBundle\Doctrine\DQL; use Doctrine\ORM\Query\AST\Functions\FunctionNode; -use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; @@ -73,13 +72,13 @@ abstract class AddressPart extends FunctionNode public function parse(Parser $parser) { - $a = $parser->match(Lexer::T_IDENTIFIER); - $parser->match(Lexer::T_OPEN_PARENTHESIS); + $a = $parser->match(\Doctrine\ORM\Query\TokenType::T_IDENTIFIER); + $parser->match(\Doctrine\ORM\Query\TokenType::T_OPEN_PARENTHESIS); // person id $this->pid = $parser->SingleValuedPathExpression(); - $parser->match(Lexer::T_COMMA); + $parser->match(\Doctrine\ORM\Query\TokenType::T_COMMA); // date $this->date = $parser->ArithmeticPrimary(); - $parser->match(Lexer::T_CLOSE_PARENTHESIS); + $parser->match(\Doctrine\ORM\Query\TokenType::T_CLOSE_PARENTHESIS); } } diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php index 4f9c480d0..a7e178097 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php @@ -555,7 +555,7 @@ class AccompanyingPeriod implements // ensure continuity of histories $criteria = new Criteria(); - $criteria->orderBy(['startDate' => Criteria::ASC, 'id' => Criteria::ASC]); + $criteria->orderBy(['startDate' => \Doctrine\Common\Collections\Order::Ascending, 'id' => \Doctrine\Common\Collections\Order::Ascending]); /** @var \Iterator $locations */ $locations = $this->getLocationHistories()->matching($criteria)->getIterator(); @@ -1536,7 +1536,7 @@ class AccompanyingPeriod implements { // ensure continuity of histories $criteria = new Criteria(); - $criteria->orderBy(['startDate' => Criteria::ASC, 'id' => Criteria::ASC]); + $criteria->orderBy(['startDate' => \Doctrine\Common\Collections\Order::Ascending, 'id' => \Doctrine\Common\Collections\Order::Ascending]); /** @var \Iterator $steps */ $steps = $this->getStepHistories()->matching($criteria)->getIterator(); diff --git a/src/Bundle/ChillPersonBundle/Entity/Person.php b/src/Bundle/ChillPersonBundle/Entity/Person.php index cd28da9a7..6f22aacc8 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Person.php +++ b/src/Bundle/ChillPersonBundle/Entity/Person.php @@ -1205,7 +1205,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI ->where( $expr->eq('shareHousehold', false) ) - ->orderBy(['startDate' => Criteria::DESC]); + ->orderBy(['startDate' => \Doctrine\Common\Collections\Order::Descending]); return $this->getHouseholdParticipations() ->matching($criteria); @@ -1227,7 +1227,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI ->where( $expr->eq('shareHousehold', true) ) - ->orderBy(['startDate' => Criteria::DESC, 'id' => Criteria::DESC]); + ->orderBy(['startDate' => \Doctrine\Common\Collections\Order::Descending, 'id' => \Doctrine\Common\Collections\Order::Descending]); return $this->getHouseholdParticipations() ->matching($criteria); diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ClosingDateAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ClosingDateAggregator.php index 6a9cce351..ab6bb6170 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ClosingDateAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ClosingDateAggregator.php @@ -48,7 +48,7 @@ final readonly class ClosingDateAggregator implements AggregatorInterface public function getLabels($key, array $values, mixed $data) { - return function (string|null $value): string { + return function (?string $value): string { if ('_header' === $value) { return 'export.aggregator.course.by_closing_date.header'; } diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/OpeningDateAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/OpeningDateAggregator.php index 6f9ea4859..d0d121c2a 100644 --- a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/OpeningDateAggregator.php +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/OpeningDateAggregator.php @@ -48,7 +48,7 @@ final readonly class OpeningDateAggregator implements AggregatorInterface public function getLabels($key, array $values, mixed $data) { - return function (string|null $value): string { + return function (?string $value): string { if ('_header' === $value) { return 'export.aggregator.course.by_opening_date.header'; } diff --git a/src/Bundle/ChillPersonBundle/Tests/Repository/AccompanyingPeriodACLAwareRepositoryTest.php b/src/Bundle/ChillPersonBundle/Tests/Repository/AccompanyingPeriodACLAwareRepositoryTest.php index 609ee03fb..8442ee106 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Repository/AccompanyingPeriodACLAwareRepositoryTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Repository/AccompanyingPeriodACLAwareRepositoryTest.php @@ -521,7 +521,7 @@ class AccompanyingPeriodACLAwareRepositoryTest extends KernelTestCase /** * @param array $scopes */ - private function buildPeriod(Person $person, array $scopes, User|null $creator, bool $confirm): AccompanyingPeriod + private function buildPeriod(Person $person, array $scopes, ?User $creator, bool $confirm): AccompanyingPeriod { $period = new AccompanyingPeriod(); $period->addPerson($person); diff --git a/src/Bundle/ChillTaskBundle/Entity/SingleTask.php b/src/Bundle/ChillTaskBundle/Entity/SingleTask.php index 25b5008b0..5e8f8eb0b 100644 --- a/src/Bundle/ChillTaskBundle/Entity/SingleTask.php +++ b/src/Bundle/ChillTaskBundle/Entity/SingleTask.php @@ -112,7 +112,7 @@ class SingleTask extends AbstractTask * message="An end date is required if a warning interval is set" * ) */ - private \DateInterval|null $warningInterval = null; + private ?\DateInterval $warningInterval = null; public function __construct() { @@ -122,7 +122,7 @@ class SingleTask extends AbstractTask /** * Get endDate. */ - public function getEndDate(): \DateTime|null + public function getEndDate(): ?\DateTime { return $this->endDate; } @@ -184,7 +184,7 @@ class SingleTask extends AbstractTask /** * Get warningInterval. */ - public function getWarningInterval(): \DateInterval|null + public function getWarningInterval(): ?\DateInterval { return $this->warningInterval; } @@ -234,7 +234,7 @@ class SingleTask extends AbstractTask * * @return SingleTask */ - public function setWarningInterval(\DateInterval|null $warningInterval) + public function setWarningInterval(?\DateInterval $warningInterval) { $this->warningInterval = $warningInterval; From d29415317b4ad1d1f354517afc56d12d061b9534 Mon Sep 17 00:00:00 2001 From: LenaertsJ Date: Thu, 7 Mar 2024 21:08:00 +0000 Subject: [PATCH 12/26] Allow users to display news on homepage (+ configuring a dashboard homepage) --- .gitlab-ci.yml | 2 +- .../Controller/AdminController.php | 8 + .../Controller/DashboardApiController.php | 52 +++++ .../Controller/NewsItemApiController.php | 53 +++++ .../Controller/NewsItemController.php | 27 +++ .../Controller/NewsItemHistoryController.php | 73 +++++++ .../ChillMainExtension.php | 32 +++ .../Entity/DashboardConfigItem.php | 112 +++++++++++ .../ChillMainBundle/Entity/NewsItem.php | 128 ++++++++++++ .../ChillMainBundle/Form/NewsItemType.php | 56 ++++++ .../Repository/NewsItemRepository.php | 145 ++++++++++++++ .../Resources/public/module/news/index.js | 1 + .../Resources/public/module/news/index.scss | 7 + .../ChillMainBundle/Resources/public/types.ts | 8 + .../public/vuejs/HomepageWidget/App.vue | 9 +- .../HomepageWidget/DashboardWidgets/News.vue | 47 +++++ .../DashboardWidgets/NewsItem.vue | 183 ++++++++++++++++++ .../public/vuejs/HomepageWidget/MyCustoms.vue | 135 +++++++------ .../public/vuejs/HomepageWidget/js/i18n.js | 10 +- .../public/vuejs/HomepageWidget/js/store.js | 8 +- .../AddressDetails/AddressDetailsButton.vue | 16 +- .../Resources/public/vuejs/_js/i18n.ts | 2 +- .../views/Admin/indexDashboard.html.twig | 13 ++ .../views/CRUD/_view_title.html.twig | 2 +- .../Resources/views/NewsItem/_list.html.twig | 30 +++ .../Resources/views/NewsItem/_show.html.twig | 15 ++ .../Resources/views/NewsItem/delete.html.twig | 6 + .../Resources/views/NewsItem/edit.html.twig | 11 ++ .../Resources/views/NewsItem/index.html.twig | 43 ++++ .../Resources/views/NewsItem/new.html.twig | 11 ++ .../NewsItem/news_items_history.html.twig | 70 +++++++ .../Resources/views/NewsItem/show.html.twig | 24 +++ .../views/NewsItem/view_admin.html.twig | 34 ++++ .../MenuBuilder/AdminNewsMenuBuilder.php | 47 +++++ .../MenuBuilder/SectionMenuBuilder.php | 8 + .../Templating/Entity/NewsItemRender.php | 35 ++++ .../Controller/NewsItemApiControllerTest.php | 39 ++++ .../Controller/NewsItemControllerTest.php | 96 +++++++++ .../NewsItemsHistoryControllerTest.php | 97 ++++++++++ .../Repository/NewsItemRepositoryTest.php | 108 +++++++++++ .../ChillMainBundle/chill.api.specs.yaml | 65 +++++++ .../ChillMainBundle/chill.webpack.config.js | 1 + .../config/services/templating.yaml | 2 + .../migrations/Version20231108141141.php | 55 ++++++ .../translations/messages.fr.yml | 28 ++- 45 files changed, 1873 insertions(+), 81 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Controller/DashboardApiController.php create mode 100644 src/Bundle/ChillMainBundle/Controller/NewsItemApiController.php create mode 100644 src/Bundle/ChillMainBundle/Controller/NewsItemController.php create mode 100644 src/Bundle/ChillMainBundle/Controller/NewsItemHistoryController.php create mode 100644 src/Bundle/ChillMainBundle/Entity/DashboardConfigItem.php create mode 100644 src/Bundle/ChillMainBundle/Entity/NewsItem.php create mode 100644 src/Bundle/ChillMainBundle/Form/NewsItemType.php create mode 100644 src/Bundle/ChillMainBundle/Repository/NewsItemRepository.php create mode 100644 src/Bundle/ChillMainBundle/Resources/public/module/news/index.js create mode 100644 src/Bundle/ChillMainBundle/Resources/public/module/news/index.scss create mode 100644 src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/DashboardWidgets/News.vue create mode 100644 src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/DashboardWidgets/NewsItem.vue create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Admin/indexDashboard.html.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/NewsItem/_list.html.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/NewsItem/_show.html.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/NewsItem/delete.html.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/NewsItem/edit.html.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/NewsItem/index.html.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/NewsItem/new.html.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/NewsItem/news_items_history.html.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/NewsItem/show.html.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/NewsItem/view_admin.html.twig create mode 100644 src/Bundle/ChillMainBundle/Routing/MenuBuilder/AdminNewsMenuBuilder.php create mode 100644 src/Bundle/ChillMainBundle/Templating/Entity/NewsItemRender.php create mode 100644 src/Bundle/ChillMainBundle/Tests/Controller/NewsItemApiControllerTest.php create mode 100644 src/Bundle/ChillMainBundle/Tests/Controller/NewsItemControllerTest.php create mode 100644 src/Bundle/ChillMainBundle/Tests/Controller/NewsItemsHistoryControllerTest.php create mode 100644 src/Bundle/ChillMainBundle/Tests/Repository/NewsItemRepositoryTest.php create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20231108141141.php diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 945d13532..43f687649 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -35,7 +35,7 @@ variables: # force a timezone TZ: Europe/Brussels # avoid direct deprecations (using symfony phpunit bridge: https://symfony.com/doc/4.x/components/phpunit_bridge.html#internal-deprecations - SYMFONY_DEPRECATIONS_HELPER: max[total]=99999999&max[self]=0&max[direct]=0&verbose=0 + SYMFONY_DEPRECATIONS_HELPER: max[total]=99999999&max[self]=0&max[direct]=0&verbose=1 stages: - Composer install diff --git a/src/Bundle/ChillMainBundle/Controller/AdminController.php b/src/Bundle/ChillMainBundle/Controller/AdminController.php index 7d3826823..46fbfb351 100644 --- a/src/Bundle/ChillMainBundle/Controller/AdminController.php +++ b/src/Bundle/ChillMainBundle/Controller/AdminController.php @@ -47,4 +47,12 @@ class AdminController extends AbstractController { return $this->render('@ChillMain/Admin/indexUser.html.twig'); } + + /** + * @Route("/{_locale}/admin/dashboard", name="chill_main_dashboard_admin") + */ + public function indexDashboardAction() + { + return $this->render('@ChillMain/Admin/indexDashboard.html.twig'); + } } diff --git a/src/Bundle/ChillMainBundle/Controller/DashboardApiController.php b/src/Bundle/ChillMainBundle/Controller/DashboardApiController.php new file mode 100644 index 000000000..8a6027ff3 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/DashboardApiController.php @@ -0,0 +1,52 @@ +newsItemRepository->countCurrentNews()) { + // show news only if we have news + // NOTE: maybe this should be done in the frontend... + $data[] = + [ + 'position' => 'top-left', + 'id' => 1, + 'type' => 'news', + 'metadata' => [ + // arbitrary data that will be store "some time" + 'only_unread' => false, + ], + ]; + } + + return new JsonResponse($data, JsonResponse::HTTP_OK, []); + } +} diff --git a/src/Bundle/ChillMainBundle/Controller/NewsItemApiController.php b/src/Bundle/ChillMainBundle/Controller/NewsItemApiController.php new file mode 100644 index 000000000..786a7e0f1 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/NewsItemApiController.php @@ -0,0 +1,53 @@ +newsItemRepository->countCurrentNews(); + $paginator = $this->paginatorFactory->create($total); + $newsItems = $this->newsItemRepository->findCurrentNews( + $paginator->getItemsPerPage(), + $paginator->getCurrentPage()->getFirstItemNumber() + ); + + return new JsonResponse($this->serializer->serialize( + new Collection(array_values($newsItems), $paginator), + 'json', + [ + AbstractNormalizer::GROUPS => ['read'], + ] + ), JsonResponse::HTTP_OK, [], true); + } +} diff --git a/src/Bundle/ChillMainBundle/Controller/NewsItemController.php b/src/Bundle/ChillMainBundle/Controller/NewsItemController.php new file mode 100644 index 000000000..a94dd55b9 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/NewsItemController.php @@ -0,0 +1,27 @@ +addOrderBy('e.startDate', 'DESC'); + $query->addOrderBy('e.id', 'DESC'); + + return parent::orderQuery($action, $query, $request, $paginator); + } +} diff --git a/src/Bundle/ChillMainBundle/Controller/NewsItemHistoryController.php b/src/Bundle/ChillMainBundle/Controller/NewsItemHistoryController.php new file mode 100644 index 000000000..cf1f4922b --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/NewsItemHistoryController.php @@ -0,0 +1,73 @@ +buildFilterOrder(); + $total = $this->newsItemRepository->countAllFilteredBySearchTerm($filter->getQueryString()); + $newsItems = $this->newsItemRepository->findAllFilteredBySearchTerm($filter->getQueryString()); + + $pagination = $this->paginatorFactory->create($total); + + return new Response($this->environment->render('@ChillMain/NewsItem/news_items_history.html.twig', [ + 'entities' => $newsItems, + 'paginator' => $pagination, + 'filter_order' => $filter, + ])); + } + + /** + * @Route("/{_locale}/news-items/{id}", name="chill_main_single_news_item") + */ + public function showSingleItem(NewsItem $newsItem, Request $request): Response + { + return new Response($this->environment->render( + '@ChillMain/NewsItem/show.html.twig', + [ + 'entity' => $newsItem, + ] + )); + } + + private function buildFilterOrder(): FilterOrderHelper + { + $filterBuilder = $this->filterOrderHelperFactory + ->create(self::class) + ->addSearchBox(); + + return $filterBuilder->build(); + } +} diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index b62f1f2d7..6dfae2fbd 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -19,6 +19,7 @@ use Chill\MainBundle\Controller\CountryController; use Chill\MainBundle\Controller\LanguageController; use Chill\MainBundle\Controller\LocationController; use Chill\MainBundle\Controller\LocationTypeController; +use Chill\MainBundle\Controller\NewsItemController; use Chill\MainBundle\Controller\RegroupmentController; use Chill\MainBundle\Controller\UserController; use Chill\MainBundle\Controller\UserJobApiController; @@ -53,6 +54,7 @@ use Chill\MainBundle\Entity\GeographicalUnitLayer; use Chill\MainBundle\Entity\Language; use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Entity\LocationType; +use Chill\MainBundle\Entity\NewsItem; use Chill\MainBundle\Entity\Regroupment; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\UserJob; @@ -62,6 +64,7 @@ use Chill\MainBundle\Form\CountryType; use Chill\MainBundle\Form\LanguageType; use Chill\MainBundle\Form\LocationFormType; use Chill\MainBundle\Form\LocationTypeType; +use Chill\MainBundle\Form\NewsItemType; use Chill\MainBundle\Form\RegroupmentType; use Chill\MainBundle\Form\UserJobType; use Chill\MainBundle\Form\UserType; @@ -544,6 +547,35 @@ class ChillMainExtension extends Extension implements ], ], ], + [ + 'class' => NewsItem::class, + 'name' => 'news_item', + 'base_path' => '/admin/news_item', + 'form_class' => NewsItemType::class, + 'controller' => NewsItemController::class, + 'actions' => [ + 'index' => [ + 'role' => 'ROLE_ADMIN', + 'template' => '@ChillMain/NewsItem/index.html.twig', + ], + 'new' => [ + 'role' => 'ROLE_ADMIN', + 'template' => '@ChillMain/NewsItem/new.html.twig', + ], + 'view' => [ + 'role' => 'ROLE_ADMIN', + 'template' => '@ChillMain/NewsItem/view_admin.html.twig', + ], + 'edit' => [ + 'role' => 'ROLE_ADMIN', + 'template' => '@ChillMain/NewsItem/edit.html.twig', + ], + 'delete' => [ + 'role' => 'ROLE_ADMIN', + 'template' => '@ChillMain/NewsItem/delete.html.twig', + ], + ], + ], ], 'apis' => [ [ diff --git a/src/Bundle/ChillMainBundle/Entity/DashboardConfigItem.php b/src/Bundle/ChillMainBundle/Entity/DashboardConfigItem.php new file mode 100644 index 000000000..ed9cc07bf --- /dev/null +++ b/src/Bundle/ChillMainBundle/Entity/DashboardConfigItem.php @@ -0,0 +1,112 @@ +id; + } + + public function getType(): string + { + return $this->type; + } + + public function setType(string $type): self + { + $this->type = $type; + + return $this; + } + + public function getPosition(): string + { + return $this->position; + } + + public function setPosition(string $position): void + { + $this->position = $position; + } + + public function getUser(): User + { + return $this->user; + } + + public function setUser(User $user): void + { + $this->user = $user; + } + + public function getMetadata(): array + { + return $this->metadata; + } + + public function setMetadata(array $metadata): void + { + $this->metadata = $metadata; + } +} diff --git a/src/Bundle/ChillMainBundle/Entity/NewsItem.php b/src/Bundle/ChillMainBundle/Entity/NewsItem.php new file mode 100644 index 000000000..604c58c5f --- /dev/null +++ b/src/Bundle/ChillMainBundle/Entity/NewsItem.php @@ -0,0 +1,128 @@ +title; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function getContent(): string + { + return $this->content; + } + + public function setContent(string $content): void + { + $this->content = $content; + } + + public function getStartDate(): ?\DateTimeImmutable + { + return $this->startDate; + } + + public function setStartDate(?\DateTimeImmutable $startDate): void + { + $this->startDate = $startDate; + } + + public function getEndDate(): ?\DateTimeImmutable + { + return $this->endDate; + } + + public function setEndDate(?\DateTimeImmutable $endDate): void + { + $this->endDate = $endDate; + } + + public function getId(): ?int + { + return $this->id; + } +} diff --git a/src/Bundle/ChillMainBundle/Form/NewsItemType.php b/src/Bundle/ChillMainBundle/Form/NewsItemType.php new file mode 100644 index 000000000..b6a93a0a0 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Form/NewsItemType.php @@ -0,0 +1,56 @@ +add('title', TextType::class, [ + 'required' => true, + ]) + ->add('content', ChillTextareaType::class, [ + 'required' => false, + ]) + ->add( + 'startDate', + ChillDateType::class, + [ + 'required' => true, + 'input' => 'datetime_immutable', + 'label' => 'news.startDate', + ] + ) + ->add('endDate', ChillDateType::class, [ + 'required' => false, + 'input' => 'datetime_immutable', + 'label' => 'news.endDate', + ]); + } + + /** + * @return void + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefault('data_class', NewsItem::class); + } +} diff --git a/src/Bundle/ChillMainBundle/Repository/NewsItemRepository.php b/src/Bundle/ChillMainBundle/Repository/NewsItemRepository.php new file mode 100644 index 000000000..4552e2a92 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Repository/NewsItemRepository.php @@ -0,0 +1,145 @@ +repository = $entityManager->getRepository(NewsItem::class); + } + + public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder + { + return $this->repository->createQueryBuilder($alias, $indexBy); + } + + public function find($id) + { + return $this->repository->find($id); + } + + public function findAll() + { + return $this->repository->findAll(); + } + + public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null) + { + return $this->repository->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria) + { + return $this->repository->findOneBy($criteria); + } + + public function getClassName() + { + return NewsItem::class; + } + + private function buildBaseQuery( + ?string $pattern = null + ): QueryBuilder { + $qb = $this->createQueryBuilder('n'); + + $qb->where('n.startDate <= :now'); + $qb->setParameter('now', $this->clock->now()); + + if (null !== $pattern && '' !== $pattern) { + $qb->andWhere($qb->expr()->like('LOWER(UNACCENT(n.title))', 'LOWER(UNACCENT(:pattern))')) + ->orWhere($qb->expr()->like('LOWER(UNACCENT(n.content))', 'LOWER(UNACCENT(:pattern))')) + ->setParameter('pattern', '%'.$pattern.'%'); + } + + return $qb; + } + + public function findAllFilteredBySearchTerm(?string $pattern = null) + { + $qb = $this->buildBaseQuery($pattern); + $qb + ->addOrderBy('n.startDate', 'DESC') + ->addOrderBy('n.id', 'DESC'); + + return $qb->getQuery()->getResult(); + } + + /** + * @return list + */ + public function findCurrentNews(?int $limit = null, ?int $offset = null): array + { + $qb = $this->buildQueryCurrentNews(); + $qb->addOrderBy('n.startDate', 'DESC'); + + if (null !== $limit) { + $qb->setMaxResults($limit); + } + + if (null !== $offset) { + $qb->setFirstResult($offset); + } + + return $qb + ->getQuery() + ->getResult(); + } + + public function countAllFilteredBySearchTerm(?string $pattern = null) + { + $qb = $this->buildBaseQuery($pattern); + + return $qb + ->select('COUNT(n)') + ->getQuery() + ->getSingleScalarResult(); + } + + public function countCurrentNews() + { + return $this->buildQueryCurrentNews() + ->select('COUNT(n)') + ->getQuery() + ->getSingleScalarResult(); + } + + private function buildQueryCurrentNews(): QueryBuilder + { + $now = $this->clock->now(); + + $qb = $this->createQueryBuilder('n'); + $qb + ->where( + $qb->expr()->andX( + $qb->expr()->lte('n.startDate', ':now'), + $qb->expr()->orX( + $qb->expr()->gt('n.endDate', ':now'), + $qb->expr()->isNull('n.endDate') + ) + ) + ) + ->setParameter('now', $now); + + return $qb; + } +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/module/news/index.js b/src/Bundle/ChillMainBundle/Resources/public/module/news/index.js new file mode 100644 index 000000000..67aac616f --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/module/news/index.js @@ -0,0 +1 @@ +import './index.scss'; diff --git a/src/Bundle/ChillMainBundle/Resources/public/module/news/index.scss b/src/Bundle/ChillMainBundle/Resources/public/module/news/index.scss new file mode 100644 index 000000000..7b65cda80 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/module/news/index.scss @@ -0,0 +1,7 @@ +div.flex-table { + .news-content { + p { + margin-top: 1rem; + } + } +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/types.ts b/src/Bundle/ChillMainBundle/Resources/public/types.ts index b31b70897..2e33b8248 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/types.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/types.ts @@ -160,3 +160,11 @@ export interface LocationType { contactData: "optional" | "required"; title: TranslatableString; } + +export interface NewsItemType { + id: number; + title: string; + content: string; + startDate: DateTime; + endDate: DateTime | null; +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/App.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/App.vue index 315fd863f..02763221a 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/App.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/App.vue @@ -97,6 +97,8 @@ import MyNotifications from './MyNotifications'; import MyWorkflows from './MyWorkflows.vue'; import TabCounter from './TabCounter'; import { mapState } from "vuex"; +import { makeFetch } from "ChillMainAssets/lib/api/apiMethods"; + export default { name: "App", @@ -112,7 +114,7 @@ export default { }, data() { return { - activeTab: 'MyCustoms' + activeTab: 'MyCustoms', } }, computed: { @@ -126,8 +128,11 @@ export default { }, methods: { selectTab(tab) { - this.$store.dispatch('getByTab', { tab: tab }); + if (tab !== 'MyCustoms') { + this.$store.dispatch('getByTab', { tab: tab }); + } this.activeTab = tab; + console.log(this.activeTab) } }, mounted() { diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/DashboardWidgets/News.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/DashboardWidgets/News.vue new file mode 100644 index 000000000..ff1cae89b --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/DashboardWidgets/News.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/DashboardWidgets/NewsItem.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/DashboardWidgets/NewsItem.vue new file mode 100644 index 000000000..bbad4315c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/DashboardWidgets/NewsItem.vue @@ -0,0 +1,183 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyCustoms.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyCustoms.vue index 5e9cb79df..43ad31c45 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyCustoms.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/MyCustoms.vue @@ -1,76 +1,73 @@ @@ -98,4 +103,10 @@ span.counter { background-color: unset; } } - \ No newline at end of file + +div.news { + max-height: 22rem; + overflow: hidden; + overflow-y: scroll; +} + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/i18n.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/i18n.js index c4c97e5c6..ddc1b55ef 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/i18n.js +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/i18n.js @@ -63,7 +63,15 @@ const appMessages = { }, emergency: "Urgent", confidential: "Confidentiel", - automatic_notification: "Notification automatique" + automatic_notification: "Notification automatique", + widget: { + news: { + title: "Actualités", + readMore: "Lire la suite", + date: "Date", + none: "Aucune actualité" + } + } } }; diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/store.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/store.js index 088cb93b7..1579a3d0c 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/store.js +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/HomepageWidget/js/store.js @@ -96,13 +96,11 @@ const store = createStore({ }, catchError(state, error) { state.errorMsg.push(error); - } + }, }, actions: { getByTab({ commit, getters }, { tab, param }) { switch (tab) { - case 'MyCustoms': - break; // case 'MyWorks': // if (!getters.isWorksLoaded) { // commit('setLoading', true); @@ -221,8 +219,8 @@ const store = createStore({ default: throw 'tab '+ tab; } - } + }, }, }); -export { store }; \ No newline at end of file +export { store }; diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsButton.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsButton.vue index f5e914d27..9a1550d7d 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsButton.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsButton.vue @@ -15,18 +15,20 @@ import AddressModal from "./AddressModal.vue"; export interface AddressModalContentProps { address_id: number; - address_ref_status: AddressRefStatus | null; + address_ref_status: AddressRefStatus; } -const data = reactive<{ - loading: boolean, - working_address: Address | null, - working_ref_status: AddressRefStatus | null, -}>({ +interface AddressModalData { + loading: boolean, + working_address: Address | null, + working_ref_status: AddressRefStatus | null, +} + +const data: AddressModalData = reactive({ loading: false, working_address: null, working_ref_status: null, -}); +} as AddressModalData); const props = defineProps(); diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.ts b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.ts index c509ac10f..f81699a7c 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.ts @@ -51,7 +51,7 @@ const messages = { years_old: "1 an | {n} an | {n} ans", residential_address: "Adresse de résidence", located_at: "réside chez" - } + }, } }; diff --git a/src/Bundle/ChillMainBundle/Resources/views/Admin/indexDashboard.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Admin/indexDashboard.html.twig new file mode 100644 index 000000000..9c7513d4c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Admin/indexDashboard.html.twig @@ -0,0 +1,13 @@ +{% extends "@ChillMain/Admin/layoutWithVerticalMenu.html.twig" %} + +{% block vertical_menu_content %} + {{ chill_menu('admin_news_item', { + 'layout': '@ChillMain/Admin/menu_admin_section.html.twig', + }) }} +{% endblock %} + +{% block layout_wvm_content %} + {% block admin_content %} +

{{ 'admin.dashboard.description' | trans }}

+ {% endblock %} +{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_title.html.twig b/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_title.html.twig index 3473dd298..00688ecd9 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_title.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/CRUD/_view_title.html.twig @@ -1 +1 @@ -{{ 'crud.%crud_name%.title_view'|trans({'%crud_name%' : crud_name }) }} \ No newline at end of file +{{ ('crud.' ~ crud_name ~ '.title_view')|trans({'%crud_name%' : crud_name }) }} diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/_list.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/_list.html.twig new file mode 100644 index 000000000..11ca95995 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/_list.html.twig @@ -0,0 +1,30 @@ +
+
+

+ {{ entity.title }} +

+
+
+

+ {% if entity.startDate %} + {{ entity.startDate|format_date('long') }} + {% endif %} + {% if entity.endDate %} + - {{ entity.endDate|format_date('long') }} + {% endif %} +

+
+
+
+ {{ entity.content|u.truncate(350, '… [' ~ ('news.read_more'|trans) ~ '](' ~ chill_path_add_return_path('chill_main_single_news_item', { 'id': entity.id } ) ~ ')', false)|chill_markdown_to_html }} +
+
+
+
    +
  • + +
  • +
+
+
+ diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/_show.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/_show.html.twig new file mode 100644 index 000000000..5cd830bbc --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/_show.html.twig @@ -0,0 +1,15 @@ +
+
+

+ {{ entity.startDate|format_date('long') }} + {% if entity.endDate is not null %} + - {{ entity.endDate|format_date('long') }} + {% endif %} +

+
+
+
+ {{ entity.content|chill_markdown_to_html }} +
+
+
diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/delete.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/delete.html.twig new file mode 100644 index 000000000..28efd4748 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/delete.html.twig @@ -0,0 +1,6 @@ +{% extends '@ChillMain/CRUD/Admin/index.html.twig' %} + +{% block admin_content %} + {% embed '@ChillMain/CRUD/_delete_content.html.twig' %} + {% endembed %} +{% endblock admin_content %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/edit.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/edit.html.twig new file mode 100644 index 000000000..4d55c480c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/edit.html.twig @@ -0,0 +1,11 @@ +{% extends '@ChillMain/CRUD/Admin/index.html.twig' %} + +{% block title %} + {% include('@ChillMain/CRUD/_edit_title.html.twig') %} +{% endblock %} + +{% block admin_content %} + {% embed '@ChillMain/CRUD/_edit_content.html.twig' %} + {% block content_form_actions_save_and_show %}{% endblock %} + {% endembed %} +{% endblock admin_content %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/index.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/index.html.twig new file mode 100644 index 000000000..0a197353b --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/index.html.twig @@ -0,0 +1,43 @@ +{% extends '@ChillMain/CRUD/Admin/index.html.twig' %} + +{% block admin_content %} + {% embed '@ChillMain/CRUD/_index.html.twig' %} + {% block table_entities_thead_tr %} + {{ 'Title'|trans }} + {{ 'news.startDate'|trans }} + {{ 'news.endDate'|trans }} + {% endblock %} + {% block table_entities_tbody %} + {% for entity in entities %} + + {{ entity.title }} + {{ entity.startDate|format_date('long') }} + {% if entity.endDate is not null %} + {{ entity.endDate|format_date('long') }} + {% else %} + {{ 'news.noDate'|trans }} + {% endif %} + +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+ + + {% endfor %} + {% endblock %} + + {% block actions_before %} +
  • + {{'Back to the admin'|trans}} +
  • + {% endblock %} + {% endembed %} +{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/new.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/new.html.twig new file mode 100644 index 000000000..7c204dddd --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/new.html.twig @@ -0,0 +1,11 @@ +{% extends '@ChillMain/CRUD/Admin/index.html.twig' %} + +{% block title %} + {% include('@ChillMain/CRUD/_new_title.html.twig') %} +{% endblock %} + +{% block admin_content %} + {% embed '@ChillMain/CRUD/_new_content.html.twig' %} + {% block content_form_actions_save_and_show %}{% endblock %} + {% endembed %} +{% endblock admin_content %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/news_items_history.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/news_items_history.html.twig new file mode 100644 index 000000000..ff93a9e09 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/news_items_history.html.twig @@ -0,0 +1,70 @@ +{% extends "@ChillMain/layout.html.twig" %} + +{% block title %} + {{ 'news.title'|trans }} +{% endblock title %} + +{% block css %} + {{ parent() }} + {{ encore_entry_link_tags('mod_news') }} +{% endblock %} + +{% block content %} +
    +

    {{ 'news.title'|trans }}

    + + {{ filter_order|chill_render_filter_order_helper }} + + {% if entities|length == 0 %} +

    + {{ "news.no_data"|trans }} +

    + {% else %} + +
    + + {% for entity in entities %} + +
    +
    +
    +
    +
    +

    {{ entity.title }}

    +
    +
    +

    + {% if entity.startDate %} + {{ entity.startDate|format_date('long') }} + {% endif %} + {% if entity.endDate %} + - {{ entity.endDate|format_date('long') }} + {% endif %} +

    +
    +
    +
    +
    + +
    +
    + {{ entity.content|u.truncate(350, '…', false)|chill_markdown_to_html }} +
    +
    + {% if entity.content|length > 350 %} + + {% endif %} +
    + {% endfor %} +
    + + {{ chill_pagination(paginator) }} + {% endif %} +
    +{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/show.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/show.html.twig new file mode 100644 index 000000000..a718a2121 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/show.html.twig @@ -0,0 +1,24 @@ +{% extends '@ChillMain/layout.html.twig' %} + +{% block title 'news.show_details'|trans %} + +{% block css %} + {{ parent() }} + {{ encore_entry_link_tags('mod_news') }} +{% endblock %} + +{% block content %} +
    +
    +

    {{ entity.title }}

    + + {% include '@ChillMain/NewsItem/_show.html.twig' with { 'entity': entity } %} + + +
    +
    +{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/NewsItem/view_admin.html.twig b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/view_admin.html.twig new file mode 100644 index 000000000..92ed2c235 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/NewsItem/view_admin.html.twig @@ -0,0 +1,34 @@ +{% extends '@ChillMain/CRUD/Admin/index.html.twig' %} + +{% block title %} + {% include('@ChillMain/CRUD/_view_title.html.twig') %} +{% endblock %} + +{% block css %} + {{ parent() }} + {{ encore_entry_link_tags('mod_news') }} +{% endblock %} + +{% block admin_content %} + +
    +
    +

    {{ entity.title }}

    + + {% include '@ChillMain/NewsItem/_show.html.twig' with { 'entity': entity } %} + + +
    +
    + +{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Routing/MenuBuilder/AdminNewsMenuBuilder.php b/src/Bundle/ChillMainBundle/Routing/MenuBuilder/AdminNewsMenuBuilder.php new file mode 100644 index 000000000..0ce6a9824 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Routing/MenuBuilder/AdminNewsMenuBuilder.php @@ -0,0 +1,47 @@ +authorizationChecker->isGranted('ROLE_ADMIN')) { + return; + } + + $menu->addChild('admin.dashboard.title', [ + 'route' => 'chill_main_dashboard_admin', + ]) + ->setAttribute('class', 'list-group-item-header') + ->setExtras([ + 'order' => 9000, + ]); + + $menu->addChild('admin.dashboard.news', [ + 'route' => 'chill_crud_news_item_index', + ])->setExtras(['order' => 9000]); + } + + public static function getMenuIds(): array + { + return ['admin_section', 'admin_news_item']; + } +} diff --git a/src/Bundle/ChillMainBundle/Routing/MenuBuilder/SectionMenuBuilder.php b/src/Bundle/ChillMainBundle/Routing/MenuBuilder/SectionMenuBuilder.php index 6247cf769..a8deba828 100644 --- a/src/Bundle/ChillMainBundle/Routing/MenuBuilder/SectionMenuBuilder.php +++ b/src/Bundle/ChillMainBundle/Routing/MenuBuilder/SectionMenuBuilder.php @@ -60,6 +60,14 @@ class SectionMenuBuilder implements LocalMenuBuilderInterface 'order' => 20, ]); } + + $menu->addChild($this->translator->trans('news.menu'), [ + 'route' => 'chill_main_news_items_history', + ]) + ->setExtras([ + 'icons' => ['newspaper-o'], + 'order' => 5, + ]); } public static function getMenuIds(): array diff --git a/src/Bundle/ChillMainBundle/Templating/Entity/NewsItemRender.php b/src/Bundle/ChillMainBundle/Templating/Entity/NewsItemRender.php new file mode 100644 index 000000000..fd2e2e211 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Templating/Entity/NewsItemRender.php @@ -0,0 +1,35 @@ + + */ +final readonly class NewsItemRender implements ChillEntityRenderInterface +{ + public function renderBox($entity, array $options): string + { + return ''; + } + + public function renderString($entity, array $options): string + { + return $entity->getTitle(); + } + + public function supports($newsItem, array $options): bool + { + return $newsItem instanceof NewsItem; + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemApiControllerTest.php b/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemApiControllerTest.php new file mode 100644 index 000000000..f8f7aafea --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemApiControllerTest.php @@ -0,0 +1,39 @@ +getClientAuthenticated(); + + $client->request('GET', '/api/1.0/main/news/current.json'); + $this->assertResponseIsSuccessful('Testing whether the GET request to the news item Api endpoint was successful'); + + $responseContent = json_decode($client->getResponse()->getContent(), true, 512, JSON_THROW_ON_ERROR); + + if (!empty($responseContent['data'][0])) { + $this->assertArrayHasKey('title', $responseContent['data'][0]); + } + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemControllerTest.php b/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemControllerTest.php new file mode 100644 index 000000000..5aa515fd1 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemControllerTest.php @@ -0,0 +1,96 @@ + + */ + private static array $entitiesToDelete = []; + + private readonly EntityManagerInterface $em; + + protected function tearDown(): void + { + self::ensureKernelShutdown(); + } + + public static function tearDownAfterClass(): void + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + foreach (self::$entitiesToDelete as [$class, $id]) { + $entity = $em->find($class, $id); + + if (null !== $entity) { + $em->remove($entity); + } + } + + $em->flush(); + } + + public static function generateNewsItemIds(): iterable + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + $newsItem = new NewsItem(); + $newsItem->setTitle('Lorem Ipsum'); + $newsItem->setContent('some text'); + $newsItem->setStartDate(new \DateTimeImmutable('now')); + + $em->persist($newsItem); + $em->flush(); + + self::$entitiesToDelete[] = [NewsItem::class, $newsItem]; + + self::ensureKernelShutdown(); + + yield [$newsItem]; + } + + public function testList() + { + $client = $this->getClientAuthenticated('admin', 'password'); + $client->request('GET', '/fr/admin/news_item'); + + self::assertResponseIsSuccessful('News item admin page shows'); + } + + /** + * @dataProvider generateNewsItemIds + */ + public function testShowSingleItem(NewsItem $newsItem) + { + $client = $this->getClientAuthenticated('admin', 'password'); + $client->request('GET', "/fr/admin/news_item/{$newsItem->getId()}/view"); + + self::assertResponseIsSuccessful('Single news item admin page loads successfully'); + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemsHistoryControllerTest.php b/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemsHistoryControllerTest.php new file mode 100644 index 000000000..19da9ac18 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Controller/NewsItemsHistoryControllerTest.php @@ -0,0 +1,97 @@ + + */ + private static array $toDelete = []; + + public static function tearDownAfterClass(): void + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + foreach (static::$toDelete as [$class, $entity]) { + $query = $em->createQuery(sprintf('DELETE FROM %s e WHERE e.id = :id', $class)) + ->setParameter('id', $entity->getId()); + $query->execute(); + } + + static::$toDelete = []; + + self::ensureKernelShutdown(); + } + + protected function tearDown(): void + { + self::ensureKernelShutdown(); + } + + public static function generateNewsItemIds(): iterable + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + $news = new NewsItem(); + + $news->setContent('test content'); + $news->setTitle('Title'); + $news->setStartDate(new \DateTimeImmutable('yesterday')); + + $em->persist($news); + $em->flush(); + + static::$toDelete[] = [NewsItem::class, $news]; + + self::ensureKernelShutdown(); + + yield [$news->getId()]; + } + + public function testList() + { + self::ensureKernelShutdown(); + $client = $this->getClientAuthenticated(); + + $client->request('GET', '/fr/news-items/history'); + + self::assertResponseIsSuccessful('Test that /fr/news-items history shows'); + } + + /** + * @dataProvider generateNewsItemIds + */ + public function testShowSingleItem(int $newsItemId) + { + self::ensureKernelShutdown(); + $client = $this->getClientAuthenticated(); + + $client->request('GET', "/fr/news-items/{$newsItemId}"); + + $this->assertResponseIsSuccessful('test that single news item page loads successfully'); + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Repository/NewsItemRepositoryTest.php b/src/Bundle/ChillMainBundle/Tests/Repository/NewsItemRepositoryTest.php new file mode 100644 index 000000000..7aab97e48 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Repository/NewsItemRepositoryTest.php @@ -0,0 +1,108 @@ + + */ + private array $toDelete = []; + + protected function setUp(): void + { + self::bootKernel(); + $this->entityManager = self::$container->get(EntityManagerInterface::class); + } + + protected function tearDown(): void + { + foreach ($this->toDelete as [$class, $entity]) { + $query = $this->entityManager->createQuery(sprintf('DELETE FROM %s e WHERE e.id = :id', $class)) + ->setParameter('id', $entity->getId()); + $query->execute(); + } + + $this->toDelete = []; + } + + private function getNewsItemsRepository(ClockInterface $clock): NewsItemRepository + { + return new NewsItemRepository($this->entityManager, $clock); + } + + public function testFindCurrentNews() + { + $clock = new MockClock($now = new \DateTimeImmutable('2023-01-10')); + $repository = $this->getNewsItemsRepository($clock); + + $newsItem1 = new NewsItem(); + $newsItem1->setTitle('This is a mock news item'); + $newsItem1->setContent('We are testing that the repository returns the correct news items'); + $newsItem1->setStartDate(new \DateTimeImmutable('2023-01-01')); + $newsItem1->setEndDate(new \DateTimeImmutable('2023-01-05')); + + $newsItem2 = new NewsItem(); + $newsItem2->setTitle('This is a mock news item'); + $newsItem2->setContent('We are testing that the repository returns the correct news items'); + $newsItem2->setStartDate(new \DateTimeImmutable('2023-01-01')); + $newsItem2->setEndDate($now->add(new \DateInterval('P1D'))); + + $newsItem3 = new NewsItem(); + $newsItem3->setTitle('This is a mock news item'); + $newsItem3->setContent('We are testing that the repository returns the correct news items'); + $newsItem3->setStartDate(new \DateTimeImmutable('2033-11-03')); + $newsItem3->setEndDate(null); + + $newsItem4 = new NewsItem(); + $newsItem4->setTitle('This is a mock news item'); + $newsItem4->setContent('We are testing that the repository returns the correct news items'); + $newsItem4->setStartDate(new \DateTimeImmutable('2023-01-03')); + $newsItem4->setEndDate(null); + + $this->entityManager->persist($newsItem1); + $this->entityManager->persist($newsItem2); + $this->entityManager->persist($newsItem3); + $this->entityManager->persist($newsItem4); + $this->entityManager->flush(); + + $this->toDelete = [ + [NewsItem::class, $newsItem1], + [NewsItem::class, $newsItem2], + [NewsItem::class, $newsItem3], + [NewsItem::class, $newsItem4], + ]; + + // Call the method to test + $result = $repository->findCurrentNews(); + + // Assertions + $this->assertCount(2, $result); + $this->assertInstanceOf(NewsItem::class, $result[0]); + $this->assertContains($newsItem2, $result); + $this->assertContains($newsItem4, $result); + } +} diff --git a/src/Bundle/ChillMainBundle/chill.api.specs.yaml b/src/Bundle/ChillMainBundle/chill.api.specs.yaml index 98e0e915e..f37ee723d 100644 --- a/src/Bundle/ChillMainBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillMainBundle/chill.api.specs.yaml @@ -10,6 +10,12 @@ servers: components: schemas: + Date: + type: object + properties: + datetime: + type: string + format: date-time User: type: object properties: @@ -131,6 +137,35 @@ components: id: type: integer + DashboardConfigItem: + type: object + properties: + id: + type: integer + type: + type: string + metadata: + type: object + userId: + type: integer + position: + type: string + + NewsItem: + type: object + properties: + id: + type: integer + title: + type: string + content: + type: string + startDate: + $ref: "#/components/schemas/Date" + endDate: + $ref: "#/components/schemas/Date" + + paths: /1.0/search.json: get: @@ -842,4 +877,34 @@ paths: $ref: '#/components/schemas/Workflow' 403: description: "Unauthorized" + /1.0/main/dashboard-config-item.json: + get: + tags: + - dashboard config item + summary: Returns the dashboard configuration for the current user. + responses: + 200: + description: "ok" + content: + application/json: + schema: + $ref: '#/components/schemas/DashboardConfigItem' + 403: + description: "Unauthorized" + /1.0/main/news/current.json: + get: + tags: + - news items + summary: Returns a list of news items which are valid + responses: + 200: + description: "ok" + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/NewsItem' + 403: + description: "Unauthorized" diff --git a/src/Bundle/ChillMainBundle/chill.webpack.config.js b/src/Bundle/ChillMainBundle/chill.webpack.config.js index 792d0a27e..7e0060892 100644 --- a/src/Bundle/ChillMainBundle/chill.webpack.config.js +++ b/src/Bundle/ChillMainBundle/chill.webpack.config.js @@ -76,6 +76,7 @@ module.exports = function(encore, entries) encore.addEntry('mod_pick_postal_code', __dirname + '/Resources/public/module/pick-postal-code/index.js'); encore.addEntry('mod_pick_rolling_date', __dirname + '/Resources/public/module/pick-rolling-date/index.js'); encore.addEntry('mod_address_details', __dirname + '/Resources/public/module/address-details/index'); + encore.addEntry('mod_news', __dirname + '/Resources/public/module/news/index.js'); // Vue entrypoints encore.addEntry('vue_address', __dirname + '/Resources/public/vuejs/Address/index.js'); diff --git a/src/Bundle/ChillMainBundle/config/services/templating.yaml b/src/Bundle/ChillMainBundle/config/services/templating.yaml index e69700732..0baa91b69 100644 --- a/src/Bundle/ChillMainBundle/config/services/templating.yaml +++ b/src/Bundle/ChillMainBundle/config/services/templating.yaml @@ -47,6 +47,8 @@ services: Chill\MainBundle\Templating\Entity\AddressRender: ~ + Chill\MainBundle\Templating\Entity\NewsItemRender: ~ + Chill\MainBundle\Templating\Entity\UserRender: ~ Chill\MainBundle\Templating\Listing\: diff --git a/src/Bundle/ChillMainBundle/migrations/Version20231108141141.php b/src/Bundle/ChillMainBundle/migrations/Version20231108141141.php new file mode 100644 index 000000000..d4fe9b561 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20231108141141.php @@ -0,0 +1,55 @@ +addSql('CREATE SEQUENCE chill_main_dashboard_config_item_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE chill_main_news_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE chill_main_dashboard_config_item (id INT NOT NULL, user_id INT DEFAULT NULL, type VARCHAR(255) NOT NULL, position VARCHAR(255) NOT NULL, metadata JSONB DEFAULT \'{}\'::jsonb, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_CF59DFD6A76ED395 ON chill_main_dashboard_config_item (user_id)'); + $this->addSql('CREATE TABLE chill_main_news (id INT NOT NULL, title TEXT NOT NULL, content TEXT NOT NULL, startDate DATE NOT NULL, endDate DATE DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_96922AFB3174800F ON chill_main_news (createdBy_id)'); + $this->addSql('CREATE INDEX IDX_96922AFB65FF1AEC ON chill_main_news (updatedBy_id)'); + $this->addSql('COMMENT ON COLUMN chill_main_news.startDate IS \'(DC2Type:date_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_main_news.endDate IS \'(DC2Type:date_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_main_news.createdAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_main_news.updatedAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE chill_main_dashboard_config_item ADD CONSTRAINT FK_CF59DFD6A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_news ADD CONSTRAINT FK_96922AFB3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_news ADD CONSTRAINT FK_96922AFB65FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP SEQUENCE chill_main_dashboard_config_item_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE chill_main_news_id_seq CASCADE'); + $this->addSql('ALTER TABLE chill_main_dashboard_config_item DROP CONSTRAINT FK_CF59DFD6A76ED395'); + $this->addSql('ALTER TABLE chill_main_news DROP CONSTRAINT FK_96922AFB3174800F'); + $this->addSql('ALTER TABLE chill_main_news DROP CONSTRAINT FK_96922AFB65FF1AEC'); + $this->addSql('DROP TABLE chill_main_dashboard_config_item'); + $this->addSql('DROP TABLE chill_main_news'); + } +} diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index 91068275f..421cac473 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -82,7 +82,6 @@ Comment: Commentaire Comments: Commentaires Pinned comment: Commentaire épinglé Any comment: Aucun commentaire -Read more: Lire la suite (more...): (suite...) # comment embeddable @@ -438,6 +437,16 @@ crud: add_new: Ajouter un centre title_new: Nouveau centre title_edit: Modifier un centre + news_item: + index: + title: Liste des actualités + add_new: Créer une nouvelle actualité + title_new: Nouvelle actualité + title_view: Voir l'actualité + title_edit: Modifier une actualité + title_delete: Supprimer une actualité + button_delete: Supprimer + confirm_message_delete: Êtes-vous sûr de vouloir supprimer l'actualité, "%as_string%" ? No entities: Aucun élément @@ -679,3 +688,20 @@ admin: undefined: non défini user: Utilisateur scope: Service + dashboard: + title: Tableau de bord + news: Actualités + description: Configuration du tableau de bord + + +news: + noDate: Pas de date de fin + startDate: Date de début de publication + endDate: Date de fin de publication sur la page d'accueil + title: Historique des actualités + menu: Actualités + no_data: Aucune actualité + read_more: Lire la suite + show_details: Voir l'actualité + + From 5880858191336a0c30e16b3ed743db3e61e59c87 Mon Sep 17 00:00:00 2001 From: LenaertsJ Date: Fri, 8 Mar 2024 10:37:43 +0000 Subject: [PATCH 13/26] =?UTF-8?q?Resolve=20"Nouveau=20filtre:=20Filtrer=20?= =?UTF-8?q?les=20actions=20ayant=20re=C3=A7u=20une=20nouvelle=20=C3=A9valu?= =?UTF-8?q?ation=20cr=C3=A9=C3=A9e=20entre=20deux=20dates"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unreleased/Feature-20240227-133135.yaml | 6 ++ ...odWorkWithEvaluationBetweenDatesFilter.php | 85 +++++++++++++++++++ .../WithEvaluationBetweenDatesFilterTest.php | 63 ++++++++++++++ .../services/exports_social_actions.yaml | 4 + .../translations/messages.fr.yml | 5 ++ 5 files changed, 163 insertions(+) create mode 100644 .changes/unreleased/Feature-20240227-133135.yaml create mode 100644 src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkWithEvaluationBetweenDatesFilter.php create mode 100644 src/Bundle/ChillPersonBundle/Tests/Export/Filter/SocialWorkFilters/WithEvaluationBetweenDatesFilterTest.php diff --git a/.changes/unreleased/Feature-20240227-133135.yaml b/.changes/unreleased/Feature-20240227-133135.yaml new file mode 100644 index 000000000..9935f9916 --- /dev/null +++ b/.changes/unreleased/Feature-20240227-133135.yaml @@ -0,0 +1,6 @@ +kind: Feature +body: New export filter for social actions with an evaluation created between two + dates +time: 2024-02-27T13:31:35.828320711+01:00 +custom: + Issue: "237" diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkWithEvaluationBetweenDatesFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkWithEvaluationBetweenDatesFilter.php new file mode 100644 index 000000000..439cb47ca --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/AccompanyingPeriodWorkWithEvaluationBetweenDatesFilter.php @@ -0,0 +1,85 @@ +add('start_date', PickRollingDateType::class, [ + 'label' => 'export.filter.work.evaluation_between_dates.start_date', + ]) + ->add('end_date', PickRollingDateType::class, [ + 'label' => 'export.filter.work.evaluation_between_dates.end_date', + ]); + } + + public function getFormDefaultData(): array + { + return ['start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)]; + } + + public function getTitle(): string + { + return 'export.filter.work.evaluation_between_dates.title'; + } + + public function describeAction($data, $format = 'string'): array + { + return [ + 'export.filter.work.evaluation_between_dates.description', + [ + '%startDate%' => null !== $data['start_date'] ? $this->rollingDateConverter->convert($data['start_date'])->format('d-m-Y') : '', + '%endDate%' => null !== $data['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 + { + $s = 'workeval_between_filter_start'; + $e = 'workeval_between_filter_end'; + + $qb->andWhere( + $qb->expr()->exists( + 'SELECT 1 FROM '.AccompanyingPeriodWorkEvaluation::class." workeval_between_filter_workeval WHERE workeval_between_filter_workeval.createdAt BETWEEN :{$s} AND :{$e} AND IDENTITY(workeval_between_filter_workeval.accompanyingPeriodWork) = acpw.id" + ) + ) + ->setParameter($s, $this->rollingDateConverter->convert($data['start_date'])) + ->setParameter($e, $this->rollingDateConverter->convert($data['end_date'])); + } + + public function applyOn(): string + { + return Declarations::SOCIAL_WORK_ACTION_TYPE; + } +} diff --git a/src/Bundle/ChillPersonBundle/Tests/Export/Filter/SocialWorkFilters/WithEvaluationBetweenDatesFilterTest.php b/src/Bundle/ChillPersonBundle/Tests/Export/Filter/SocialWorkFilters/WithEvaluationBetweenDatesFilterTest.php new file mode 100644 index 000000000..680eb4cc9 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Export/Filter/SocialWorkFilters/WithEvaluationBetweenDatesFilterTest.php @@ -0,0 +1,63 @@ +filter = self::$container->get(AccompanyingPeriodWorkWithEvaluationBetweenDatesFilter::class); + } + + public function getFilter() + { + return $this->filter; + } + + public function getFormData() + { + return [ + [ + 'start_date' => new RollingDate(RollingDate::T_MONTH_CURRENT_START), + 'end_date' => new RollingDate(RollingDate::T_TODAY), + ], + ]; + } + + public function getQueryBuilders() + { + self::bootKernel(); + + $em = self::$container->get(EntityManagerInterface::class); + + return [ + $em->createQueryBuilder() + ->select('acpw.id') + ->from(AccompanyingPeriodWork::class, 'acpw'), + ]; + } +} diff --git a/src/Bundle/ChillPersonBundle/config/services/exports_social_actions.yaml b/src/Bundle/ChillPersonBundle/config/services/exports_social_actions.yaml index 1987568e3..9e199679f 100644 --- a/src/Bundle/ChillPersonBundle/config/services/exports_social_actions.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/exports_social_actions.yaml @@ -79,6 +79,10 @@ services: tags: - { name: chill.export_filter, alias: social_work_actions_creator_scope_filter } + Chill\PersonBundle\Export\Filter\SocialWorkFilters\AccompanyingPeriodWorkWithEvaluationBetweenDatesFilter: + tags: + - { name: chill.export_filter, alias: social_work_actions_evaluation_btw_dates_filter } + ## AGGREGATORS chill.person.export.aggregator_action_type: diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 50c6a87d5..dc1d89b55 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -1291,6 +1291,11 @@ export: by_creator_scope: title: Filtrer les actions par service du créateur "Filtered by creator scope: only %scopes%": "Filtré par service du créateur: uniquement %scopes%" + evaluation_between_dates: + title: Filtrer les actions associées à une évaluation créée entre deux dates + description: Uniquement les actions associées à une évaluation créée entre le %startDate% et le %endDate% + start_date: Date de début + end_date: Date de fin list: person_with_acp: From d7f4895248d91e365db1ea0c6cdd72cf1bc3177c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 14 Mar 2024 15:46:32 +0100 Subject: [PATCH 14/26] Fix the command which load language The command load the languages in the configured languages in chill's configuration. --- .changes/unreleased/Fixed-20240314-154343.yaml | 6 ++++++ .../Command/LoadAndUpdateLanguagesCommand.php | 9 +++++---- src/Bundle/ChillMainBundle/config/services/command.yaml | 3 --- .../ChillPersonBundle/translations/messages.fr.yml | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 .changes/unreleased/Fixed-20240314-154343.yaml diff --git a/.changes/unreleased/Fixed-20240314-154343.yaml b/.changes/unreleased/Fixed-20240314-154343.yaml new file mode 100644 index 000000000..47fe9d598 --- /dev/null +++ b/.changes/unreleased/Fixed-20240314-154343.yaml @@ -0,0 +1,6 @@ +kind: Fixed +body: 'Fix languages: load the languages in all availables languages configured for + Chill' +time: 2024-03-14T15:43:43.396044369+01:00 +custom: + Issue: "264" diff --git a/src/Bundle/ChillMainBundle/Command/LoadAndUpdateLanguagesCommand.php b/src/Bundle/ChillMainBundle/Command/LoadAndUpdateLanguagesCommand.php index 860f8c82e..4c8cc4eeb 100644 --- a/src/Bundle/ChillMainBundle/Command/LoadAndUpdateLanguagesCommand.php +++ b/src/Bundle/ChillMainBundle/Command/LoadAndUpdateLanguagesCommand.php @@ -12,11 +12,12 @@ declare(strict_types=1); namespace Chill\MainBundle\Command; use Chill\MainBundle\Entity\Language; -use Doctrine\ORM\EntityManager; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\Intl\Languages; /* @@ -39,7 +40,7 @@ class LoadAndUpdateLanguagesCommand extends Command /** * LoadCountriesCommand constructor. */ - public function __construct(private readonly EntityManager $entityManager, private $availableLanguages) + public function __construct(private readonly EntityManagerInterface $entityManager, private ParameterBagInterface $parameterBag) { parent::__construct(); } @@ -79,7 +80,7 @@ class LoadAndUpdateLanguagesCommand extends Command protected function execute(InputInterface $input, OutputInterface $output): int { $em = $this->entityManager; - $chillAvailableLanguages = $this->availableLanguages; + $chillAvailableLanguages = $this->parameterBag->get('chill_main.available_languages'); $languages = []; foreach ($chillAvailableLanguages as $avLang) { @@ -113,7 +114,7 @@ class LoadAndUpdateLanguagesCommand extends Command $avLangNames = []; foreach ($chillAvailableLanguages as $avLang) { - $avLangNames[$avLang] = Languages::getName($code, $avLang); + $avLangNames[$avLang] = ucfirst(Languages::getName($code, $avLang)); } $languageDB->setName($avLangNames); diff --git a/src/Bundle/ChillMainBundle/config/services/command.yaml b/src/Bundle/ChillMainBundle/config/services/command.yaml index 29d6d4bf1..8a2327c0b 100644 --- a/src/Bundle/ChillMainBundle/config/services/command.yaml +++ b/src/Bundle/ChillMainBundle/config/services/command.yaml @@ -17,9 +17,6 @@ services: - { name: console.command } Chill\MainBundle\Command\LoadAndUpdateLanguagesCommand: - arguments: - $entityManager: '@doctrine.orm.entity_manager' - $availableLanguages: '%chill_main.available_languages%' tags: - { name: console.command } diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 50c6a87d5..364218ef2 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -57,7 +57,7 @@ Add new phone: Ajouter un numéro de téléphone Remove phone: Supprimer 'Notes on contact information': 'Remarques sur les informations de contact' 'Remarks': 'Remarques' -Spoken languages': 'Langues parlées' +Spoken languages: 'Langues parlées' 'Unknown spoken languages': 'Langues parlées inconnues' Male: Homme Female: Femme From 0ff51b0a5cc2db13277995beee90d366feef8ded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 14 Mar 2024 15:05:30 +0000 Subject: [PATCH 15/26] Force new parameter to be readonly in LoadAndUpdateLanguagesCommand constructor --- .../ChillMainBundle/Command/LoadAndUpdateLanguagesCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillMainBundle/Command/LoadAndUpdateLanguagesCommand.php b/src/Bundle/ChillMainBundle/Command/LoadAndUpdateLanguagesCommand.php index 4c8cc4eeb..e4b3ae673 100644 --- a/src/Bundle/ChillMainBundle/Command/LoadAndUpdateLanguagesCommand.php +++ b/src/Bundle/ChillMainBundle/Command/LoadAndUpdateLanguagesCommand.php @@ -40,7 +40,7 @@ class LoadAndUpdateLanguagesCommand extends Command /** * LoadCountriesCommand constructor. */ - public function __construct(private readonly EntityManagerInterface $entityManager, private ParameterBagInterface $parameterBag) + public function __construct(private readonly EntityManagerInterface $entityManager, private readonly ParameterBagInterface $parameterBag) { parent::__construct(); } From df0afcd2285cd25b68e48b5385561b27eb15885e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 14 Mar 2024 21:16:05 +0100 Subject: [PATCH 16/26] Fix activity filter inconsistency in document generation This commit resolves issue 259 where the filtering of activities differed within the document generation and in the list of activities for an accompanying period. This amendment to the Chill Activity Bundle ensures consistent behavior. Additionally, new test methods and query adjustments were applied to the ActivityACLAwareRepository for better functionality. --- .../unreleased/Fixed-20240314-211532.yaml | 7 + .../Repository/ActivityACLAwareRepository.php | 4 +- ...tActivitiesByAccompanyingPeriodContext.php | 20 ++- .../ActivityACLAwareRepositoryTest.php | 28 +++- ...ivitiesByAccompanyingPeriodContextTest.php | 139 ++++++++++++++++++ 5 files changed, 194 insertions(+), 4 deletions(-) create mode 100644 .changes/unreleased/Fixed-20240314-211532.yaml create mode 100644 src/Bundle/ChillActivityBundle/Tests/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContextTest.php diff --git a/.changes/unreleased/Fixed-20240314-211532.yaml b/.changes/unreleased/Fixed-20240314-211532.yaml new file mode 100644 index 000000000..dc3e675cd --- /dev/null +++ b/.changes/unreleased/Fixed-20240314-211532.yaml @@ -0,0 +1,7 @@ +kind: Fixed +body: Keep a consistent behaviour between the filtering of activities within the document + generation (model "accompanying period with activities"), and the same filter in + the list of activities for an accompanying period +time: 2024-03-14T21:15:32.690077164+01:00 +custom: + Issue: "259" diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php index 1f50f7d62..6d9991e6d 100644 --- a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php +++ b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php @@ -243,7 +243,8 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos thirdparties.thirdpartyids, persons.personids, actions.socialactionids, - issues.socialissueids + issues.socialissueids, + a.user_id FROM activity a LEFT JOIN chill_main_location location ON a.location_id = location.id @@ -283,6 +284,7 @@ final readonly class ActivityACLAwareRepository implements ActivityACLAwareRepos ->addJoinedEntityResult(ActivityPresence::class, 'activityPresence', 'a', 'attendee') ->addFieldResult('activityPresence', 'presence_id', 'id') ->addFieldResult('activityPresence', 'presence_name', 'name') + ->addScalarResult('user_id', 'userId', Types::INTEGER) // results which cannot be mapped into entity ->addScalarResult('comment_comment', 'comment', Types::TEXT) diff --git a/src/Bundle/ChillActivityBundle/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContext.php b/src/Bundle/ChillActivityBundle/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContext.php index 33675754a..baf1c279d 100644 --- a/src/Bundle/ChillActivityBundle/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContext.php +++ b/src/Bundle/ChillActivityBundle/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContext.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\ActivityBundle\Service\DocGenerator; +use Chill\ActivityBundle\Entity\Activity; use Chill\ActivityBundle\Entity\ActivityPresence; use Chill\ActivityBundle\Entity\ActivityType; use Chill\ActivityBundle\Repository\ActivityACLAwareRepositoryInterface; @@ -112,7 +113,7 @@ class ListActivitiesByAccompanyingPeriodContext implements } /** - * @return list + * @return list */ private function filterActivitiesByUser(array $activities, User $user): array { @@ -120,6 +121,12 @@ class ListActivitiesByAccompanyingPeriodContext implements array_filter( $activities, function ($activity) use ($user) { + $u = $activity['user']; + + if (null !== $u && $u['username'] === $user->getUsername()) { + return true; + } + $activityUsernames = array_map(static fn ($user) => $user['username'], $activity['users'] ?? []); return \in_array($user->getUsername(), $activityUsernames, true); @@ -129,7 +136,7 @@ class ListActivitiesByAccompanyingPeriodContext implements } /** - * @return list + * @return list */ private function filterWorksByUser(array $works, User $user): array { @@ -216,6 +223,15 @@ class ListActivitiesByAccompanyingPeriodContext implements foreach ($activities as $row) { $activity = $row[0]; + $user = match (null === $row['userId']) { + false => $this->userRepository->find($row['userId']), + true => null, + }; + + $activity['user'] = $this->normalizer->normalize($user, 'docgen', [ + AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => User::class, + ]); + $activity['date'] = $this->normalizer->normalize($activity['date'], 'docgen', [ AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => \DateTime::class, ]); diff --git a/src/Bundle/ChillActivityBundle/Tests/Repository/ActivityACLAwareRepositoryTest.php b/src/Bundle/ChillActivityBundle/Tests/Repository/ActivityACLAwareRepositoryTest.php index 88eb3e7f8..67b3c1f5d 100644 --- a/src/Bundle/ChillActivityBundle/Tests/Repository/ActivityACLAwareRepositoryTest.php +++ b/src/Bundle/ChillActivityBundle/Tests/Repository/ActivityACLAwareRepositoryTest.php @@ -91,6 +91,29 @@ class ActivityACLAwareRepositoryTest extends KernelTestCase self::assertIsArray($actual); } + /** + * @dataProvider provideDataFindByAccompanyingPeriod + */ + public function testfindByAccompanyingPeriodSimplified(AccompanyingPeriod $period, User $user, string $role, ?int $start = 0, ?int $limit = 1000, array $orderBy = ['date' => 'DESC'], array $filters = []): void + { + $security = $this->prophesize(Security::class); + $security->isGranted($role, $period)->willReturn(true); + $security->getUser()->willReturn($user); + + $repository = new ActivityACLAwareRepository( + $this->authorizationHelperForCurrentUser, + $this->centerResolverManager, + $this->activityRepository, + $this->entityManager, + $security->reveal(), + $this->requestStack + ); + + $actual = $repository->findByAccompanyingPeriodSimplified($period); + + self::assertIsArray($actual); + } + /** * @dataProvider provideDataFindByAccompanyingPeriod */ @@ -301,7 +324,10 @@ class ActivityACLAwareRepositoryTest extends KernelTestCase ->getQuery() ->getResult() ) { - throw new \RuntimeException('no jobs found'); + $job = new UserJob(); + $job->setLabel(['fr' => 'test']); + $this->entityManager->persist($job); + $this->entityManager->flush(); } if (null === $user = $this->entityManager diff --git a/src/Bundle/ChillActivityBundle/Tests/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContextTest.php b/src/Bundle/ChillActivityBundle/Tests/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContextTest.php new file mode 100644 index 000000000..ff9f30c30 --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Tests/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContextTest.php @@ -0,0 +1,139 @@ +listActivitiesByAccompanyingPeriodContext = self::$container->get(ListActivitiesByAccompanyingPeriodContext::class); + $this->accompanyingPeriodRepository = self::$container->get(AccompanyingPeriodRepository::class); + $this->userRepository = self::$container->get(UserRepositoryInterface::class); + } + + /** + * @dataProvider provideAccompanyingPeriod + */ + public function testGetDataWithoutFilteringActivityNorWorks(int $accompanyingPeriodId, int $userId): void + { + $context = $this->getContext(); + $template = new DocGeneratorTemplate(); + $template->setOptions([ + 'mainPerson' => false, + 'person1' => false, + 'person2' => false, + 'thirdParty' => false, + ]); + + $data = $context->getData( + $template, + $this->accompanyingPeriodRepository->find($accompanyingPeriodId), + ['myActivitiesOnly' => false, 'myWorksOnly' => false] + ); + + self::assertIsArray($data); + self::assertArrayHasKey('activities', $data); + self::assertIsArray($data['activities']); + self::assertGreaterThan(0, count($data['activities'])); + self::assertIsArray($data['activities'][0]); + self::assertArrayHasKey('user', $data['activities'][0]); + self::assertIsArray($data['activities'][0]['user']); + } + + /** + * @dataProvider provideAccompanyingPeriod + */ + public function testGetDataWithoutFilteringActivityByUser(int $accompanyingPeriodId, int $userId): void + { + $context = $this->getContext(); + $template = new DocGeneratorTemplate(); + $template->setOptions([ + 'mainPerson' => false, + 'person1' => false, + 'person2' => false, + 'thirdParty' => false, + ]); + + $data = $context->getData( + $template, + $this->accompanyingPeriodRepository->find($accompanyingPeriodId), + ['myActivitiesOnly' => true, 'myWorksOnly' => false, 'creator' => $this->userRepository->find($userId)] + ); + + self::assertIsArray($data); + self::assertArrayHasKey('activities', $data); + self::assertIsArray($data['activities']); + self::assertGreaterThan(0, count($data['activities'])); + self::assertIsArray($data['activities'][0]); + self::assertArrayHasKey('user', $data['activities'][0]); + self::assertIsArray($data['activities'][0]['user']); + } + + public static function provideAccompanyingPeriod(): array + { + self::bootKernel(); + $em = self::$container->get(EntityManagerInterface::class); + + if (null === $period = $em->createQuery('SELECT a FROM '.AccompanyingPeriod::class.' a') + ->setMaxResults(1) + ->getSingleResult()) { + throw new \RuntimeException('no period found'); + } + + if (null === $user = $em->createQuery('SELECT u FROM '.User::class.' u') + ->setMaxResults(1) + ->getSingleResult() + ) { + throw new \RuntimeException('no user found'); + } + + $activity = new Activity(); + $activity + ->setAccompanyingPeriod($period) + ->setUser($user) + ->setDate(new \DateTime()); + + $em->persist($activity); + $em->flush(); + + self::ensureKernelShutdown(); + + return [ + [$period->getId(), $user->getId()], + ]; + } + + private function getContext(): ListActivitiesByAccompanyingPeriodContext + { + return $this->listActivitiesByAccompanyingPeriodContext; + } +} From dd056efa0d2e2ef72995827cbddceac28eec2bd2 Mon Sep 17 00:00:00 2001 From: LenaertsJ Date: Thu, 14 Mar 2024 21:35:00 +0000 Subject: [PATCH 17/26] In the accompanying period list, add person's centers and duration --- .../unreleased/Feature-20240314-222634.yaml | 6 +++ .../Helper/ListAccompanyingPeriodHelper.php | 37 +++++++++++++++++++ .../translations/messages.fr.yml | 2 + 3 files changed, 45 insertions(+) create mode 100644 .changes/unreleased/Feature-20240314-222634.yaml diff --git a/.changes/unreleased/Feature-20240314-222634.yaml b/.changes/unreleased/Feature-20240314-222634.yaml new file mode 100644 index 000000000..3a484a474 --- /dev/null +++ b/.changes/unreleased/Feature-20240314-222634.yaml @@ -0,0 +1,6 @@ +kind: Feature +body: In the list of accompangying period, add the list of person's centers and the + duration of the course +time: 2024-03-14T22:26:34.103648729+01:00 +custom: + Issue: "258" diff --git a/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php b/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php index 2c2bc1d02..ba14d4e2c 100644 --- a/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php +++ b/src/Bundle/ChillPersonBundle/Export/Helper/ListAccompanyingPeriodHelper.php @@ -16,10 +16,12 @@ use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Export\Helper\DateTimeHelper; use Chill\MainBundle\Export\Helper\ExportAddressHelper; use Chill\MainBundle\Export\Helper\UserHelper; +use Chill\MainBundle\Repository\CenterRepository; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress; +use Chill\PersonBundle\Entity\Person\PersonCenterHistory; use Chill\PersonBundle\Entity\SocialWork\SocialIssue; use Chill\PersonBundle\Repository\PersonRepository; use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository; @@ -39,6 +41,8 @@ final readonly class ListAccompanyingPeriodHelper 'stepSince', 'openingDate', 'closingDate', + 'duration', + 'centers', 'referrer', 'referrerSince', 'acpParticipantPersons', @@ -83,6 +87,7 @@ final readonly class ListAccompanyingPeriodHelper private TranslatorInterface $translator, private UserHelper $userHelper, private LabelPersonHelper $labelPersonHelper, + private CenterRepository $centerRepository ) { } @@ -202,6 +207,25 @@ final readonly class ListAccompanyingPeriodHelper AccompanyingPeriod::INTENSITY_REGULAR => $this->translator->trans('regular'), default => $value, }, + 'centers' => function ($value) use ($key) { + if ('_header' === $value) { + return 'export.list.acp.'.$key; + } + + if (null === $value || '' === $value || !json_validate((string) $value)) { + return ''; + } + + return implode( + '|', + array_map( + fn ($cid) => $this->centerRepository->find($cid)->getName(), + array_unique( + json_decode((string) $value, true, 512, JSON_THROW_ON_ERROR) + ) + ) + ); + }, default => static function ($value) use ($key) { if ('_header' === $value) { return 'export.list.acp.'.$key; @@ -315,6 +339,19 @@ final readonly class ListAccompanyingPeriodHelper // social issues ->addSelect('(SELECT AGGREGATE(socialIssue.id) FROM '.SocialIssue::class.' socialIssue WHERE socialIssue MEMBER OF acp.socialIssues) AS socialIssues'); + // duration + $qb->addSelect('DATE_DIFF(COALESCE(acp.closingDate, :calcDate), acp.openingDate) AS duration'); + + // centers + $qb->addSelect( + '(SELECT AGGREGATE(IDENTITY(cppch.center)) + FROM '.AccompanyingPeriodParticipation::class.' part + JOIN '.PersonCenterHistory::class." cppch + WITH IDENTITY(cppch.person) = IDENTITY(part.person) + AND OVERLAPSI (cppch.startDate, cppch.endDate), (part.startDate, part.endDate) = 'TRUE' + WHERE part.accompanyingPeriod = acp + ) AS centers" + ); // 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 812c6eb89..013298ba5 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -1346,6 +1346,8 @@ export: requestorThirdParty: Demandeur (tiers) acpParticipantPersons: Usagers concernés acpParticipantPersonsIds: Usagers concernés (identifiants) + duration: Durée du parcours (en jours) + centers: Centres des usagers eval: List of evaluations: Liste des évaluations From ccf3324bc268c4b165134c301a9ed9fe9b616bca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 15 Mar 2024 15:02:12 +0100 Subject: [PATCH 18/26] Refactor ListPersonHelper and ListPerson to simplify process and allow to add customization of fields --- .../unreleased/Feature-20240319-172100.yaml | 5 + .../ChillPersonBundle/ChillPersonBundle.php | 3 + .../Export/Export/ListPerson.php | 110 ++--------------- .../ListPersonHavingAccompanyingPeriod.php | 91 +------------- ...istPersonWithAccompanyingPeriodDetails.php | 2 +- .../CustomizeListPersonHelperInterface.php | 36 ++++++ .../Export/Helper/ListPersonHelper.php | 114 +++++++++++------- ...ListPersonHavingAccompanyingPeriodTest.php | 6 +- .../Tests/Export/Export/ListPersonTest.php | 14 +-- .../config/services/exports_person.yaml | 4 + 10 files changed, 134 insertions(+), 251 deletions(-) create mode 100644 .changes/unreleased/Feature-20240319-172100.yaml create mode 100644 src/Bundle/ChillPersonBundle/Export/Helper/CustomizeListPersonHelperInterface.php diff --git a/.changes/unreleased/Feature-20240319-172100.yaml b/.changes/unreleased/Feature-20240319-172100.yaml new file mode 100644 index 000000000..d50de48b3 --- /dev/null +++ b/.changes/unreleased/Feature-20240319-172100.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: Allow to customize list person with new fields +time: 2024-03-19T17:21:00.873293991+01:00 +custom: + Issue: "238" diff --git a/src/Bundle/ChillPersonBundle/ChillPersonBundle.php b/src/Bundle/ChillPersonBundle/ChillPersonBundle.php index 8a48cccba..89477a47b 100644 --- a/src/Bundle/ChillPersonBundle/ChillPersonBundle.php +++ b/src/Bundle/ChillPersonBundle/ChillPersonBundle.php @@ -13,6 +13,7 @@ namespace Chill\PersonBundle; use Chill\PersonBundle\Actions\Remove\PersonMoveSqlHandlerInterface; use Chill\PersonBundle\DependencyInjection\CompilerPass\AccompanyingPeriodTimelineCompilerPass; +use Chill\PersonBundle\Export\Helper\CustomizeListPersonHelperInterface; use Chill\PersonBundle\Service\EntityInfo\AccompanyingPeriodInfoUnionQueryPartInterface; use Chill\PersonBundle\Widget\PersonListWidgetFactory; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -32,5 +33,7 @@ class ChillPersonBundle extends Bundle ->addTag('chill_person.accompanying_period_info_part'); $container->registerForAutoconfiguration(PersonMoveSqlHandlerInterface::class) ->addTag('chill_person.person_move_handler'); + $container->registerForAutoconfiguration(CustomizeListPersonHelperInterface::class) + ->addTag('chill_person.list_person_customizer'); } } diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListPerson.php b/src/Bundle/ChillPersonBundle/Export/Export/ListPerson.php index 8529fac10..37b7ca7d7 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/ListPerson.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListPerson.php @@ -14,10 +14,8 @@ namespace Chill\PersonBundle\Export\Export; use Chill\CustomFieldsBundle\CustomFields\CustomFieldChoice; use Chill\CustomFieldsBundle\Entity\CustomField; use Chill\CustomFieldsBundle\Service\CustomFieldProvider; -use Chill\MainBundle\Export\ExportElementValidatedInterface; use Chill\MainBundle\Export\FormatterInterface; use Chill\MainBundle\Export\GroupedExportInterface; -use Chill\MainBundle\Export\Helper\ExportAddressHelper; use Chill\MainBundle\Export\ListInterface; use Chill\MainBundle\Form\Type\ChillDateType; use Chill\MainBundle\Templating\TranslatableStringHelper; @@ -30,21 +28,17 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Query; use PhpOffice\PhpSpreadsheet\Shared\Date; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; -use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Validator\Constraints\Callback; -use Symfony\Component\Validator\Context\ExecutionContextInterface; /** * Render a list of people. */ -class ListPerson implements ExportElementValidatedInterface, ListInterface, GroupedExportInterface +class ListPerson implements ListInterface, GroupedExportInterface { private array $slugs = []; private readonly bool $filterStatsByCenters; public function __construct( - private readonly ExportAddressHelper $addressHelper, private readonly CustomFieldProvider $customFieldProvider, private readonly ListPersonHelper $listPersonHelper, private readonly EntityManagerInterface $entityManager, @@ -56,39 +50,6 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou public function buildForm(FormBuilderInterface $builder) { - $choices = array_combine(ListPersonHelper::FIELDS, ListPersonHelper::FIELDS); - - foreach ($this->getCustomFields() as $cf) { - $choices[$this->translatableStringHelper->localize($cf->getName())] - = - $cf->getSlug(); - } - - // Add a checkbox to select fields - $builder->add('fields', ChoiceType::class, [ - 'multiple' => true, - 'expanded' => true, - 'choices' => $choices, - 'label' => 'Fields to include in export', - 'choice_attr' => static function (string $val): array { - // add a 'data-display-target' for address fields - if (str_starts_with($val, 'address') || 'center' === $val || 'household' === $val) { - return ['data-display-target' => 'address_date']; - } - - return []; - }, - 'constraints' => [new Callback([ - 'callback' => static function ($selected, ExecutionContextInterface $context) { - if (0 === \count($selected)) { - $context->buildViolation('You must select at least one element') - ->atPath('fields') - ->addViolation(); - } - }, - ])], - ]); - // add a date field for addresses $builder->add('address_date', ChillDateType::class, [ 'label' => 'Data valid at this date', @@ -99,15 +60,7 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou public function getFormDefaultData(): array { - $choices = array_combine(ListPersonHelper::FIELDS, ListPersonHelper::FIELDS); - - foreach ($this->getCustomFields() as $cf) { - $choices[$this->translatableStringHelper->localize($cf->getName())] - = - $cf->getSlug(); - } - - return ['fields' => array_values($choices), 'address_date' => new \DateTimeImmutable()]; + return ['address_date' => new \DateTimeImmutable()]; } public function getAllowedFormattersTypes() @@ -127,7 +80,7 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou public function getLabels($key, array $values, $data) { - if (\in_array($key, $this->listPersonHelper->getAllPossibleFields(), true)) { + if (\in_array($key, $this->listPersonHelper->getAllKeys(), true)) { return $this->listPersonHelper->getLabels($key, $values, $data); } @@ -138,28 +91,12 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou { $fields = []; - foreach (ListPersonHelper::FIELDS as $key) { - if (!\in_array($key, $data['fields'], true)) { - continue; - } - - if (str_starts_with($key, 'address_fields')) { - $fields = \array_merge($fields, $this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields')); - - continue; - } - - if ('lifecycleUpdate' === $key) { - $fields = \array_merge($fields, ['createdAt', 'createdBy', 'updatedAt', 'updatedBy']); - - continue; - } - + foreach ($this->listPersonHelper->getAllKeys() as $key) { $fields[] = $key; } // add the key from slugs and return - return \array_merge($fields, \array_keys($this->slugs)); + return [...$fields, ...\array_keys($this->slugs)]; } public function getResult($query, $data) @@ -184,11 +121,6 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou { $centers = array_map(static fn ($el) => $el['center'], $acl); - // throw an error if any fields are present - if (!\array_key_exists('fields', $data)) { - throw new \Doctrine\DBAL\Exception\InvalidArgumentException('any fields have been checked'); - } - $qb = $this->entityManager->createQueryBuilder() ->from(Person::class, 'person'); @@ -202,15 +134,9 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou ->setParameter('authorized_centers', $centers); } - $fields = $data['fields']; - - $this->listPersonHelper->addSelect($qb, $fields, $data['address_date']); + $this->listPersonHelper->addSelect($qb, $data['address_date']); foreach ($this->getCustomFields() as $cf) { - if (!\in_array($cf->getSlug(), $fields, true)) { - continue; - } - $cfType = $this->customFieldProvider->getCustomFieldByType($cf->getType()); if ($cfType instanceof CustomFieldChoice && $cfType->isMultiple($cf)) { @@ -251,26 +177,6 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou return [Declarations::PERSON_TYPE]; } - public function validateForm($data, ExecutionContextInterface $context) - { - // get the field starting with address_ - $addressFields = array_filter( - ListPersonHelper::FIELDS, - static fn (string $el): bool => str_starts_with($el, 'address_') - ); - - // check if there is one field starting with address in data - if (\count(array_intersect($data['fields'], $addressFields)) > 0) { - // if a field address is checked, the date must not be empty - if (!$data['address_date'] instanceof \DateTimeImmutable) { - $context - ->buildViolation('You must set this date if an address is checked') - ->atPath('address_date') - ->addViolation(); - } - } - } - private function DQLToSlug($cleanedSlug) { return $this->slugs[$cleanedSlug]['slug']; @@ -293,9 +199,9 @@ class ListPerson implements ExportElementValidatedInterface, ListInterface, Grou { return $this->entityManager ->createQuery('SELECT cf ' - .'FROM ChillCustomFieldsBundle:CustomField cf ' + .'FROM '.CustomField::class.' cf ' .'JOIN cf.customFieldGroup g ' - .'WHERE cf.type != :title AND g.entity LIKE :entity') + .'WHERE cf.type != :title AND g.entity LIKE :entity AND cf.active = TRUE') ->setParameters([ 'title' => 'title', 'entity' => \addcslashes(Person::class, '\\'), diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonHavingAccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonHavingAccompanyingPeriod.php index e638838fb..4a44f2dc5 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonHavingAccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonHavingAccompanyingPeriod.php @@ -12,10 +12,8 @@ declare(strict_types=1); namespace Chill\PersonBundle\Export\Export; use Chill\MainBundle\Export\AccompanyingCourseExportHelper; -use Chill\MainBundle\Export\ExportElementValidatedInterface; use Chill\MainBundle\Export\FormatterInterface; use Chill\MainBundle\Export\GroupedExportInterface; -use Chill\MainBundle\Export\Helper\ExportAddressHelper; use Chill\MainBundle\Export\ListInterface; use Chill\MainBundle\Form\Type\PickRollingDateType; use Chill\MainBundle\Service\RollingDate\RollingDate; @@ -30,22 +28,18 @@ use DateTimeImmutable; use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; -use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Validator\Constraints\Callback; -use Symfony\Component\Validator\Context\ExecutionContextInterface; /** * List the persons, having an accompanying period. * * Details of the accompanying period are not included */ -final readonly class ListPersonHavingAccompanyingPeriod implements ExportElementValidatedInterface, ListInterface, GroupedExportInterface +final readonly class ListPersonHavingAccompanyingPeriod implements ListInterface, GroupedExportInterface { private bool $filterStatsByCenters; public function __construct( - private ExportAddressHelper $addressHelper, private ListPersonHelper $listPersonHelper, private EntityManagerInterface $entityManager, private RollingDateConverterInterface $rollingDateConverter, @@ -56,32 +50,6 @@ final readonly class ListPersonHavingAccompanyingPeriod implements ExportElement public function buildForm(FormBuilderInterface $builder) { - $choices = array_combine(ListPersonHelper::FIELDS, ListPersonHelper::FIELDS); - - $builder->add('fields', ChoiceType::class, [ - 'multiple' => true, - 'expanded' => true, - 'choices' => $choices, - 'label' => 'Fields to include in export', - 'choice_attr' => static function (string $val): array { - // add a 'data-display-target' for address fields - if (str_starts_with($val, 'address') || 'center' === $val || 'household' === $val) { - return ['data-display-target' => 'address_date']; - } - - return []; - }, - 'constraints' => [new Callback([ - 'callback' => static function ($selected, ExecutionContextInterface $context) { - if (0 === \count($selected)) { - $context->buildViolation('You must select at least one element') - ->atPath('fields') - ->addViolation(); - } - }, - ])], - ]); - $builder->add('address_date_rolling', PickRollingDateType::class, [ 'label' => 'Data valid at this date', 'help' => 'Data regarding center, addresses, and so on will be computed at this date', @@ -90,9 +58,7 @@ final readonly class ListPersonHavingAccompanyingPeriod implements ExportElement public function getFormDefaultData(): array { - $choices = array_combine(ListPersonHelper::FIELDS, ListPersonHelper::FIELDS); - - return ['fields' => array_values($choices), 'address_date_rolling' => new RollingDate(RollingDate::T_TODAY)]; + return ['address_date_rolling' => new RollingDate(RollingDate::T_TODAY)]; } public function getAllowedFormattersTypes() @@ -117,29 +83,7 @@ final readonly class ListPersonHavingAccompanyingPeriod implements ExportElement public function getQueryKeys($data) { - $fields = []; - - foreach (ListPersonHelper::FIELDS as $key) { - if (!\in_array($key, $data['fields'], true)) { - continue; - } - - if (str_starts_with($key, 'address_fields')) { - $fields = array_merge($fields, $this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields')); - - continue; - } - - if ('lifecycleUpdate' === $key) { - $fields = array_merge($fields, ['createdAt', 'createdBy', 'updatedAt', 'updatedBy']); - - continue; - } - - $fields[] = $key; - } - - return $fields; + return $this->listPersonHelper->getAllKeys(); } public function getResult($query, $data) @@ -164,11 +108,6 @@ final readonly class ListPersonHavingAccompanyingPeriod implements ExportElement { $centers = array_map(static fn ($el) => $el['center'], $acl); - // throw an error if any fields are present - if (!\array_key_exists('fields', $data)) { - throw new \Doctrine\DBAL\Exception\InvalidArgumentException('any fields have been checked'); - } - $qb = $this->entityManager->createQueryBuilder(); $qb->from(Person::class, 'person') @@ -185,9 +124,7 @@ final readonly class ListPersonHavingAccompanyingPeriod implements ExportElement )->setParameter('authorized_centers', $centers); } - $fields = $data['fields']; - - $this->listPersonHelper->addSelect($qb, $fields, $this->rollingDateConverter->convert($data['address_date_rolling'])); + $this->listPersonHelper->addSelect($qb, $this->rollingDateConverter->convert($data['address_date_rolling'])); AccompanyingCourseExportHelper::addClosingMotiveExclusionClause($qb); @@ -208,24 +145,4 @@ final readonly class ListPersonHavingAccompanyingPeriod implements ExportElement { return [Declarations::PERSON_TYPE, Declarations::ACP_TYPE]; } - - public function validateForm($data, ExecutionContextInterface $context) - { - // get the field starting with address_ - $addressFields = array_filter( - ListPersonHelper::FIELDS, - static fn (string $el): bool => str_starts_with($el, 'address_') - ); - - // check if there is one field starting with address in data - if (\count(array_intersect($data['fields'], $addressFields)) > 0) { - // if a field address is checked, the date must not be empty - if (!$data['address_date'] instanceof \DateTimeImmutable) { - $context - ->buildViolation('You must set this date if an address is checked') - ->atPath('address_date') - ->addViolation(); - } - } - } } diff --git a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriodDetails.php b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriodDetails.php index d29ba3fde..81cc09c8d 100644 --- a/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriodDetails.php +++ b/src/Bundle/ChillPersonBundle/Export/Export/ListPersonWithAccompanyingPeriodDetails.php @@ -120,7 +120,7 @@ final readonly class ListPersonWithAccompanyingPeriodDetails implements ListInte $this->filterListAccompanyingPeriodHelper->addFilterAccompanyingPeriods($qb, $requiredModifiers, $acl, $data); - $this->listPersonHelper->addSelect($qb, ListPersonHelper::FIELDS, $this->rollingDateConverter->convert($data['address_date'])); + $this->listPersonHelper->addSelect($qb, $this->rollingDateConverter->convert($data['address_date'])); $this->listAccompanyingPeriodHelper->addSelectClauses($qb, $this->rollingDateConverter->convert($data['address_date'])); AccompanyingCourseExportHelper::addClosingMotiveExclusionClause($qb); diff --git a/src/Bundle/ChillPersonBundle/Export/Helper/CustomizeListPersonHelperInterface.php b/src/Bundle/ChillPersonBundle/Export/Helper/CustomizeListPersonHelperInterface.php new file mode 100644 index 000000000..049f79f58 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Helper/CustomizeListPersonHelperInterface.php @@ -0,0 +1,36 @@ + + */ + private iterable $customPersonHelpers, + ) { } /** @@ -82,26 +96,34 @@ class ListPersonHelper */ public function getAllKeys(): array { - return [ - ...array_filter( - ListPersonHelper::FIELDS, - fn (string $key) => !\in_array($key, ['address_fields', 'lifecycleUpdate'], true) - ), + $keys = [ + ...ListPersonHelper::FIELDS, ...$this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields'), - ...['createdAt', 'createdBy', 'updatedAt', 'updatedBy'], ]; + + foreach ($this->customPersonHelpers as $customize) { + $keys = $customize->alterKeys($keys); + } + + return $keys; } - /** - * @param array> $fields - */ - public function addSelect(QueryBuilder $qb, array $fields, \DateTimeImmutable $computedDate): void + public function addSelect(QueryBuilder $qb, \DateTimeImmutable $computedDate): void { - foreach (ListPersonHelper::FIELDS as $f) { - if (!\in_array($f, $fields, true)) { - continue; - } + // we first add all the fields which are handled by the + $focusedFieldKeys = [ + 'personId', 'countryOfBirth', 'nationality', // 'address_fields', + 'spokenLanguages', 'household_id', 'center', // 'lifecycleUpdate', + 'genderComment', 'maritalStatus', 'maritalStatusComment', 'civility', + 'createdAt', 'createdBy', 'updatedAt', 'updatedBy', + ]; + $filteredFields = array_filter( + ListPersonHelper::FIELDS, + fn ($field) => !in_array($field, $focusedFieldKeys, true) + ); + + foreach ($this->getAllKeys() as $f) { switch ($f) { case 'personId': $qb->addSelect('person.id AS personId'); @@ -114,13 +136,6 @@ class ListPersonHelper break; - case 'address_fields': - $this->addCurrentAddressAt($qb, $computedDate); - $qb->leftJoin('personHouseholdAddress.address', 'personAddress'); - $this->addressHelper->addSelectClauses(ExportAddressHelper::F_ALL, $qb, 'personAddress', 'address_fields'); - - break; - case 'spokenLanguages': $qb->addSelect('(SELECT AGGREGATE(language.id) FROM '.Language::class.' language WHERE language MEMBER OF person.spokenLanguages) AS spokenLanguages'); @@ -154,15 +169,6 @@ class ListPersonHelper break; - case 'lifecycleUpdate': - $qb - ->addSelect('person.createdAt AS createdAt') - ->addSelect('IDENTITY(person.createdBy) AS createdBy') - ->addSelect('person.updatedAt AS updatedAt') - ->addSelect('IDENTITY(person.updatedBy) AS updatedBy'); - - break; - case 'genderComment': $qb->addSelect('person.genderComment.comment AS genderComment'); @@ -184,25 +190,47 @@ class ListPersonHelper break; default: - $qb->addSelect(sprintf('person.%s as %s', $f, $f)); + if (in_array($f, $filteredFields, true)) { + $qb->addSelect(sprintf('person.%s as %s', $f, $f)); + } } } + + // address + $this->addCurrentAddressAt($qb, $computedDate); + $qb->leftJoin('personHouseholdAddress.address', 'personAddress'); + $this->addressHelper->addSelectClauses(ExportAddressHelper::F_ALL, $qb, 'personAddress', 'address_fields'); + + // lifecycle update + $qb + ->addSelect('person.createdAt AS createdAt') + ->addSelect('IDENTITY(person.createdBy) AS createdBy') + ->addSelect('person.updatedAt AS updatedAt') + ->addSelect('IDENTITY(person.updatedBy) AS updatedBy'); + + foreach ($this->customPersonHelpers as $customPersonHelper) { + $customPersonHelper->alterSelect($qb, $computedDate); + } } /** * @return array|string[] + * + * @deprecated */ public function getAllPossibleFields(): array { - return array_merge( - self::FIELDS, - ['createdAt', 'createdBy', 'updatedAt', 'updatedBy'], - $this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields') - ); + return $this->getAllKeys(); } public function getLabels($key, array $values, $data): callable { + foreach ($this->customPersonHelpers as $customPersonHelper) { + if (null !== $callable = $customPersonHelper->getLabels($key, $values, $data)) { + return $callable; + } + } + if (str_starts_with((string) $key, 'address_fields')) { return $this->addressHelper->getLabel($key, $values, $data, 'address_fields'); } @@ -366,7 +394,7 @@ class ListPersonHelper }; default: - if (!\in_array($key, self::getAllPossibleFields(), true)) { + if (!\in_array($key, self::getAllKeys(), true)) { throw new \RuntimeException("this key is not supported by this helper: {$key}"); } diff --git a/src/Bundle/ChillPersonBundle/Tests/Export/Export/ListPersonHavingAccompanyingPeriodTest.php b/src/Bundle/ChillPersonBundle/Tests/Export/Export/ListPersonHavingAccompanyingPeriodTest.php index c050779e2..118b08443 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Export/Export/ListPersonHavingAccompanyingPeriodTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Export/Export/ListPersonHavingAccompanyingPeriodTest.php @@ -11,7 +11,6 @@ declare(strict_types=1); namespace Chill\PersonBundle\Tests\Export\Export; -use Chill\MainBundle\Export\Helper\ExportAddressHelper; use Chill\MainBundle\Service\RollingDate\RollingDate; use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface; use Chill\MainBundle\Test\Export\AbstractExportTest; @@ -35,13 +34,11 @@ class ListPersonHavingAccompanyingPeriodTest extends AbstractExportTest public function getExport() { - $addressHelper = self::$container->get(ExportAddressHelper::class); $listPersonHelper = self::$container->get(ListPersonHelper::class); $entityManager = self::$container->get(EntityManagerInterface::class); $rollingDateconverter = self::$container->get(RollingDateConverterInterface::class); yield new ListPersonHavingAccompanyingPeriod( - $addressHelper, $listPersonHelper, $entityManager, $rollingDateconverter, @@ -49,7 +46,6 @@ class ListPersonHavingAccompanyingPeriodTest extends AbstractExportTest ); yield new ListPersonHavingAccompanyingPeriod( - $addressHelper, $listPersonHelper, $entityManager, $rollingDateconverter, @@ -59,7 +55,7 @@ class ListPersonHavingAccompanyingPeriodTest extends AbstractExportTest public function getFormData() { - return [['address_date_rolling' => new RollingDate(RollingDate::T_TODAY), 'fields' => ListPersonHelper::FIELDS]]; + return [['address_date_rolling' => new RollingDate(RollingDate::T_TODAY)]]; } public function getModifiersCombination() diff --git a/src/Bundle/ChillPersonBundle/Tests/Export/Export/ListPersonTest.php b/src/Bundle/ChillPersonBundle/Tests/Export/Export/ListPersonTest.php index b398d1c91..75a8dc35c 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Export/Export/ListPersonTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Export/Export/ListPersonTest.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace Chill\PersonBundle\Tests\Export\Export; use Chill\CustomFieldsBundle\Service\CustomFieldProvider; -use Chill\MainBundle\Export\Helper\ExportAddressHelper; use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\MainBundle\Test\Export\AbstractExportTest; use Chill\PersonBundle\Export\Export\ListPerson; @@ -50,14 +49,12 @@ final class ListPersonTest extends AbstractExportTest public function getExport() { - $addressHelper = self::$container->get(ExportAddressHelper::class); $customFieldProvider = self::$container->get(CustomFieldProvider::class); $listPersonHelper = self::$container->get(ListPersonHelper::class); $entityManager = self::$container->get(EntityManagerInterface::class); $translatableStringHelper = self::$container->get(TranslatableStringHelper::class); yield new ListPerson( - $addressHelper, $customFieldProvider, $listPersonHelper, $entityManager, @@ -66,7 +63,6 @@ final class ListPersonTest extends AbstractExportTest ); yield new ListPerson( - $addressHelper, $customFieldProvider, $listPersonHelper, $entityManager, @@ -77,15 +73,7 @@ final class ListPersonTest extends AbstractExportTest public function getFormData(): iterable { - foreach ([ - ['fields' => ['id', 'firstName', 'lastName']], - ['fields' => ['id', 'birthdate', 'gender', 'memo', 'email', 'phonenumber']], - ['fields' => ['firstName', 'lastName', 'phonenumber']], - ['fields' => ['id', 'nationality']], - ['fields' => ['id', 'countryOfBirth']], - ] as $base) { - yield [...$base, 'address_date' => new \DateTimeImmutable('today')]; - } + yield ['address_date' => new \DateTimeImmutable('today')]; } public function getModifiersCombination(): array diff --git a/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml b/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml index c0ed03115..263e172d0 100644 --- a/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml @@ -16,6 +16,10 @@ services: tags: - { name: chill.export, alias: list_person } + Chill\PersonBundle\Export\Helper\ListPersonHelper: + arguments: + $customPersonHelpers: !tagged_iterator chill_person.list_person_customizer + Chill\PersonBundle\Export\Export\ListPersonHavingAccompanyingPeriod: tags: - { name: chill.export, alias: list_person_with_acp } From f78f5e841943fb169b35bb56ef2c2e03f44be7d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 19 Mar 2024 20:33:27 +0100 Subject: [PATCH 19/26] Fix cs with php-cs-fixer version 3.52 --- .../Constraint/RoleScopeScopePresenceConstraint.php | 2 +- .../ChillPersonBundle/DependencyInjection/Configuration.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Validation/Constraint/RoleScopeScopePresenceConstraint.php b/src/Bundle/ChillMainBundle/Validation/Constraint/RoleScopeScopePresenceConstraint.php index a379b9539..7528908b7 100644 --- a/src/Bundle/ChillMainBundle/Validation/Constraint/RoleScopeScopePresenceConstraint.php +++ b/src/Bundle/ChillMainBundle/Validation/Constraint/RoleScopeScopePresenceConstraint.php @@ -21,7 +21,7 @@ class RoleScopeScopePresenceConstraint extends Constraint public $messageNullRequired = 'The role "%role%" should not be associated with a scope.'; public $messagePresenceRequired = 'The role "%role%" require to be associated with ' - .'a scope.'; + .'a scope.'; public function getTargets() { diff --git a/src/Bundle/ChillPersonBundle/DependencyInjection/Configuration.php b/src/Bundle/ChillPersonBundle/DependencyInjection/Configuration.php index 3579407f2..0d935e8c0 100644 --- a/src/Bundle/ChillPersonBundle/DependencyInjection/Configuration.php +++ b/src/Bundle/ChillPersonBundle/DependencyInjection/Configuration.php @@ -22,8 +22,8 @@ use Symfony\Component\Config\Definition\ConfigurationInterface; class Configuration implements ConfigurationInterface { private string $validationBirthdateNotAfterInfos = 'The period before today during which' - .' any birthdate is not allowed. The birthdate is expressed as ISO8601 : ' - .'https://en.wikipedia.org/wiki/ISO_8601#Durations'; + .' any birthdate is not allowed. The birthdate is expressed as ISO8601 : ' + .'https://en.wikipedia.org/wiki/ISO_8601#Durations'; public function getConfigTreeBuilder() { From a2cea3df02ac0d4dee654e01ac078eb8bebe47d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 19 Mar 2024 21:00:38 +0100 Subject: [PATCH 20/26] Release 2.17.0 --- .changes/unreleased/Feature-20240227-133135.yaml | 6 ------ .changes/unreleased/Feature-20240314-222634.yaml | 6 ------ .changes/unreleased/Feature-20240319-172100.yaml | 5 ----- .changes/unreleased/Fixed-20240314-154343.yaml | 6 ------ .changes/unreleased/Fixed-20240314-211532.yaml | 7 ------- .changes/v2.17.0.md | 8 ++++++++ 6 files changed, 8 insertions(+), 30 deletions(-) delete mode 100644 .changes/unreleased/Feature-20240227-133135.yaml delete mode 100644 .changes/unreleased/Feature-20240314-222634.yaml delete mode 100644 .changes/unreleased/Feature-20240319-172100.yaml delete mode 100644 .changes/unreleased/Fixed-20240314-154343.yaml delete mode 100644 .changes/unreleased/Fixed-20240314-211532.yaml create mode 100644 .changes/v2.17.0.md diff --git a/.changes/unreleased/Feature-20240227-133135.yaml b/.changes/unreleased/Feature-20240227-133135.yaml deleted file mode 100644 index 9935f9916..000000000 --- a/.changes/unreleased/Feature-20240227-133135.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: Feature -body: New export filter for social actions with an evaluation created between two - dates -time: 2024-02-27T13:31:35.828320711+01:00 -custom: - Issue: "237" diff --git a/.changes/unreleased/Feature-20240314-222634.yaml b/.changes/unreleased/Feature-20240314-222634.yaml deleted file mode 100644 index 3a484a474..000000000 --- a/.changes/unreleased/Feature-20240314-222634.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: Feature -body: In the list of accompangying period, add the list of person's centers and the - duration of the course -time: 2024-03-14T22:26:34.103648729+01:00 -custom: - Issue: "258" diff --git a/.changes/unreleased/Feature-20240319-172100.yaml b/.changes/unreleased/Feature-20240319-172100.yaml deleted file mode 100644 index d50de48b3..000000000 --- a/.changes/unreleased/Feature-20240319-172100.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Feature -body: Allow to customize list person with new fields -time: 2024-03-19T17:21:00.873293991+01:00 -custom: - Issue: "238" diff --git a/.changes/unreleased/Fixed-20240314-154343.yaml b/.changes/unreleased/Fixed-20240314-154343.yaml deleted file mode 100644 index 47fe9d598..000000000 --- a/.changes/unreleased/Fixed-20240314-154343.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: Fixed -body: 'Fix languages: load the languages in all availables languages configured for - Chill' -time: 2024-03-14T15:43:43.396044369+01:00 -custom: - Issue: "264" diff --git a/.changes/unreleased/Fixed-20240314-211532.yaml b/.changes/unreleased/Fixed-20240314-211532.yaml deleted file mode 100644 index dc3e675cd..000000000 --- a/.changes/unreleased/Fixed-20240314-211532.yaml +++ /dev/null @@ -1,7 +0,0 @@ -kind: Fixed -body: Keep a consistent behaviour between the filtering of activities within the document - generation (model "accompanying period with activities"), and the same filter in - the list of activities for an accompanying period -time: 2024-03-14T21:15:32.690077164+01:00 -custom: - Issue: "259" diff --git a/.changes/v2.17.0.md b/.changes/v2.17.0.md new file mode 100644 index 000000000..cb2f93e26 --- /dev/null +++ b/.changes/v2.17.0.md @@ -0,0 +1,8 @@ +## v2.17.0 - 2024-03-19 +### Feature +* ([#237](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/237)) New export filter for social actions with an evaluation created between two dates +* ([#258](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/258)) In the list of accompangying period, add the list of person's centers and the duration of the course +* ([#238](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/238)) Allow to customize list person with new fields +### Fixed +* ([#264](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/264)) Fix languages: load the languages in all availables languages configured for Chill +* ([#259](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/259)) Keep a consistent behaviour between the filtering of activities within the document generation (model "accompanying period with activities"), and the same filter in the list of activities for an accompanying period From 2a58330832b65fc5e1017df4dadbf7a6701bd1b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 19 Mar 2024 20:46:50 +0000 Subject: [PATCH 21/26] Update v2.17.0.md: add missing changie line --- .changes/v2.17.0.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changes/v2.17.0.md b/.changes/v2.17.0.md index cb2f93e26..52c916bcd 100644 --- a/.changes/v2.17.0.md +++ b/.changes/v2.17.0.md @@ -3,6 +3,7 @@ * ([#237](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/237)) New export filter for social actions with an evaluation created between two dates * ([#258](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/258)) In the list of accompangying period, add the list of person's centers and the duration of the course * ([#238](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/238)) Allow to customize list person with new fields +* ([#159](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/159)) Admin can publish news on the homepage ### Fixed * ([#264](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/264)) Fix languages: load the languages in all availables languages configured for Chill * ([#259](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/259)) Keep a consistent behaviour between the filtering of activities within the document generation (model "accompanying period with activities"), and the same filter in the list of activities for an accompanying period From 4f0801961841b4f12422497a28e6589897767156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 20 Mar 2024 12:36:11 +0100 Subject: [PATCH 22/26] Fix the join in the user list (admin): show only current user job --- .changes/unreleased/Fixed-20240320-123527.yaml | 6 ++++++ src/Bundle/ChillMainBundle/Repository/UserRepository.php | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .changes/unreleased/Fixed-20240320-123527.yaml diff --git a/.changes/unreleased/Fixed-20240320-123527.yaml b/.changes/unreleased/Fixed-20240320-123527.yaml new file mode 100644 index 000000000..a9e13ae9e --- /dev/null +++ b/.changes/unreleased/Fixed-20240320-123527.yaml @@ -0,0 +1,6 @@ +kind: Fixed +body: 'Fix the join between job and user in the user list (admin): show only the current + user job' +time: 2024-03-20T12:35:27.885282599+01:00 +custom: + Issue: "267" diff --git a/src/Bundle/ChillMainBundle/Repository/UserRepository.php b/src/Bundle/ChillMainBundle/Repository/UserRepository.php index dc5a9adfe..5c5dff4d0 100644 --- a/src/Bundle/ChillMainBundle/Repository/UserRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/UserRepository.php @@ -106,8 +106,8 @@ final readonly class UserRepository implements UserRepositoryInterface FROM users u LEFT JOIN chill_main_civility civility ON u.civility_id = civility.id LEFT JOIN centers mainCenter ON u.maincenter_id = mainCenter.id - LEFT JOIN chill_main_user_job_history userJobHistory ON u.id = userJobHistory.user_id - LEFT JOIN chill_main_user_job userJob ON userJobHistory.job_id = userJob.id AND tstzrange(userJobHistory.startdate, userJobHistory.enddate) @> NOW() + LEFT JOIN chill_main_user_job_history userJobHistory ON u.id = userJobHistory.user_id AND tstzrange(userJobHistory.startdate, userJobHistory.enddate) @> NOW() + LEFT JOIN chill_main_user_job userJob ON userJobHistory.job_id = userJob.id LEFT JOIN chill_main_user_scope_history userScopeHistory ON u.id = userScopeHistory.user_id AND tstzrange(userScopeHistory.startdate, userScopeHistory.enddate) @> NOW() LEFT JOIN scopes mainScope ON userScopeHistory.scope_id = mainScope.id LEFT JOIN chill_main_location currentLocation ON u.currentlocation_id = currentLocation.id From fc88a5f40d0001b2e653f53eafce368b7c6e1091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 26 Mar 2024 17:06:49 +0000 Subject: [PATCH 23/26] Improve admin UX for configuration of document template (document generation) --- .../unreleased/Feature-20240326-170418.yaml | 5 + .../DocGeneratorTemplateController.php | 132 +++++----- .../Entity/DocGeneratorTemplate.php | 2 +- .../DocGeneratorTemplateRepository.php | 3 +- ...ocGeneratorTemplateRepositoryInterface.php | 23 ++ .../DocGeneratorTemplate/index.html.twig | 63 ++++- .../pick-context.html.twig | 12 +- .../Email/on_generation_failed_email.txt.twig | 4 +- .../Email/send_data_dump_to_admin.txt.twig | 7 + .../Service/Generator/Generator.php | 147 ++++++++---- .../Service/Generator/GeneratorInterface.php | 43 +++- ...erMessageHandledClearStoredObjectCache.php | 64 +++++ .../Service/Messenger/OnGenerationFails.php | 48 ++-- .../Messenger/RequestGenerationHandler.php | 90 ++++++- .../Messenger/RequestGenerationMessage.php | 35 ++- .../Context/Generator/GeneratorTest.php | 27 ++- ...ssageHandledClearStoredObjectCacheTest.php | 107 +++++++++ .../Messenger/OnGenerationFailsTest.php | 226 ++++++++++++++++++ .../translations/messages+intl-icu.fr.yml | 4 + .../translations/messages.fr.yml | 21 +- .../Entity/StoredObject.php | 53 ++++ .../Repository/StoredObjectRepository.php | 5 +- .../StoredObjectRepositoryInterface.php | 22 ++ .../Service/StoredObjectManager.php | 12 + .../Service/StoredObjectManagerInterface.php | 7 + .../{ => Service}/StoredObjectManagerTest.php | 37 ++- .../migrations/Version20240322100107.php | 36 +++ .../AccompanyingPeriodContext.php | 2 +- .../ChillWopiBundle/src/Controller/Editor.php | 14 +- .../unable_to_edit_such_document.html.twig | 34 +++ .../src/Service/Wopi/AuthorizationManager.php | 2 +- .../src/Service/Wopi/UserManager.php | 8 + .../src/translations/messages.fr.yml | 2 + 33 files changed, 1116 insertions(+), 181 deletions(-) create mode 100644 .changes/unreleased/Feature-20240326-170418.yaml create mode 100644 src/Bundle/ChillDocGeneratorBundle/Repository/DocGeneratorTemplateRepositoryInterface.php create mode 100644 src/Bundle/ChillDocGeneratorBundle/Resources/views/Email/send_data_dump_to_admin.txt.twig create mode 100644 src/Bundle/ChillDocGeneratorBundle/Service/Messenger/OnAfterMessageHandledClearStoredObjectCache.php create mode 100644 src/Bundle/ChillDocGeneratorBundle/tests/Service/Messenger/OnAfterMessageHandledClearStoredObjectCacheTest.php create mode 100644 src/Bundle/ChillDocGeneratorBundle/tests/Service/Messenger/OnGenerationFailsTest.php create mode 100644 src/Bundle/ChillDocGeneratorBundle/translations/messages+intl-icu.fr.yml create mode 100644 src/Bundle/ChillDocStoreBundle/Repository/StoredObjectRepositoryInterface.php rename src/Bundle/ChillDocStoreBundle/Tests/{ => Service}/StoredObjectManagerTest.php (81%) create mode 100644 src/Bundle/ChillDocStoreBundle/migrations/Version20240322100107.php create mode 100644 src/Bundle/ChillWopiBundle/src/Resources/views/Editor/unable_to_edit_such_document.html.twig create mode 100644 src/Bundle/ChillWopiBundle/src/translations/messages.fr.yml diff --git a/.changes/unreleased/Feature-20240326-170418.yaml b/.changes/unreleased/Feature-20240326-170418.yaml new file mode 100644 index 000000000..f86f55d96 --- /dev/null +++ b/.changes/unreleased/Feature-20240326-170418.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: Improve admin UX to configure document templates for document generation +time: 2024-03-26T17:04:18.351694753+01:00 +custom: + Issue: "268" diff --git a/src/Bundle/ChillDocGeneratorBundle/Controller/DocGeneratorTemplateController.php b/src/Bundle/ChillDocGeneratorBundle/Controller/DocGeneratorTemplateController.php index 788ccb59c..2449daaf4 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Controller/DocGeneratorTemplateController.php +++ b/src/Bundle/ChillDocGeneratorBundle/Controller/DocGeneratorTemplateController.php @@ -16,29 +16,42 @@ use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface; use Chill\DocGeneratorBundle\Context\Exception\ContextNotFoundException; use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository; -use Chill\DocGeneratorBundle\Service\Generator\GeneratorInterface; use Chill\DocGeneratorBundle\Service\Messenger\RequestGenerationMessage; use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Form\Type\PickUserDynamicType; use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Serializer\Model\Collection; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\Clock\ClockInterface; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; -use Symfony\Component\Form\Extension\Core\Type\FileType; +use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; // TODO à mettre dans services use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Security\Core\Security; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; +use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Constraints\NotNull; final class DocGeneratorTemplateController extends AbstractController { - public function __construct(private readonly ContextManager $contextManager, private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository, private readonly GeneratorInterface $generator, private readonly MessageBusInterface $messageBus, private readonly PaginatorFactory $paginatorFactory, private readonly EntityManagerInterface $entityManager) - { + public function __construct( + private readonly ContextManager $contextManager, + private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository, + private readonly MessageBusInterface $messageBus, + private readonly PaginatorFactory $paginatorFactory, + private readonly EntityManagerInterface $entityManager, + private readonly ClockInterface $clock, + private readonly Security $security, + ) { } /** @@ -163,9 +176,7 @@ final class DocGeneratorTemplateController extends AbstractController throw new NotFoundHttpException(sprintf('Entity with classname %s and id %s is not found', $context->getEntityClass(), $entityId)); } - $contextGenerationData = [ - 'test_file' => null, - ]; + $contextGenerationData = []; if ( $context instanceof DocGeneratorContextWithPublicFormInterface @@ -175,25 +186,39 @@ final class DocGeneratorTemplateController extends AbstractController $builder = $this->createFormBuilder( array_merge( $context->getFormData($template, $entity), - $isTest ? ['test_file' => null, 'show_data' => false] : [] + $isTest ? ['creator' => null, 'dump_only' => false, 'send_result_to' => ''] : [] ) ); $context->buildPublicForm($builder, $template, $entity); } else { $builder = $this->createFormBuilder( - ['test_file' => null, 'show_data' => false] + ['creator' => null, 'show_data' => false, 'send_result_to' => ''] ); } if ($isTest) { - $builder->add('test_file', FileType::class, [ - 'label' => 'Template file', + $builder->add('dump_only', CheckboxType::class, [ + 'label' => 'docgen.Show data instead of generating', 'required' => false, ]); - $builder->add('show_data', CheckboxType::class, [ - 'label' => 'Show data instead of generating', - 'required' => false, + $builder->add('send_result_to', EmailType::class, [ + 'label' => 'docgen.Send report to', + 'help' => 'docgen.Send report errors to this email address', + 'empty_data' => '', + 'required' => true, + 'constraints' => [ + new NotBlank(), + new NotNull(), + ], + ]); + $builder->add('creator', PickUserDynamicType::class, [ + 'label' => 'docgen.Generate as creator', + 'help' => 'docgen.The document will be generated as the given creator', + 'multiple' => false, + 'constraints' => [ + new NotNull(), + ], ]); } @@ -204,8 +229,10 @@ final class DocGeneratorTemplateController extends AbstractController } elseif (!$form->isSubmitted() || ($form->isSubmitted() && !$form->isValid())) { $templatePath = '@ChillDocGenerator/Generator/basic_form.html.twig'; $templateOptions = [ - 'entity' => $entity, 'form' => $form->createView(), - 'template' => $template, 'context' => $context, + 'entity' => $entity, + 'form' => $form->createView(), + 'template' => $template, + 'context' => $context, ]; return $this->render($templatePath, $templateOptions); @@ -218,60 +245,57 @@ final class DocGeneratorTemplateController extends AbstractController $context->contextGenerationDataNormalize($template, $entity, $contextGenerationData) : []; - // if is test, render the data or generate the doc - if ($isTest && isset($form) && $form['show_data']->getData()) { - return $this->render('@ChillDocGenerator/Generator/debug_value.html.twig', [ - 'datas' => json_encode($context->getData($template, $entity, $contextGenerationData), \JSON_PRETTY_PRINT), - ]); - } - if ($isTest) { - $generated = $this->generator->generateDocFromTemplate( - $template, - $entityId, - $contextGenerationDataSanitized, - null, - true, - isset($form) ? $form['test_file']->getData() : null - ); - - return new Response( - $generated, - Response::HTTP_OK, - [ - 'Content-Transfer-Encoding', 'binary', - 'Content-Type' => 'application/vnd.oasis.opendocument.text', - 'Content-Disposition' => 'attachment; filename="generated.odt"', - 'Content-Length' => \strlen($generated), - ], - ); - } - - // this is not a test // we prepare the object to store the document $storedObject = (new StoredObject()) ->setStatus(StoredObject::STATUS_PENDING) ; + if ($isTest) { + // document will be stored during 15 days, if generation is a test + $storedObject->setDeleteAt($this->clock->now()->add(new \DateInterval('P15D'))); + } + $this->entityManager->persist($storedObject); - // we store the generated document - $context - ->storeGenerated( - $template, - $storedObject, - $entity, - $contextGenerationData - ); + // we store the generated document (associate with the original entity, etc.) + // but only if this is not a test + if (!$isTest) { + $context + ->storeGenerated( + $template, + $storedObject, + $entity, + $contextGenerationData + ); + } $this->entityManager->flush(); + if ($isTest) { + $creator = $contextGenerationData['creator']; + $sendResultTo = ($form ?? null)?->get('send_result_to')?->getData() ?? null; + $dumpOnly = ($form ?? null)?->get('dump_only')?->getData() ?? false; + } else { + $creator = $this->security->getUser(); + + if (!$creator instanceof User) { + throw new AccessDeniedHttpException('only authenticated user can request a generation'); + } + + $sendResultTo = null; + $dumpOnly = false; + } + $this->messageBus->dispatch( new RequestGenerationMessage( - $this->getUser(), + $creator, $template, $entityId, $storedObject, $contextGenerationDataSanitized, + $isTest, + $sendResultTo, + $dumpOnly, ) ); diff --git a/src/Bundle/ChillDocGeneratorBundle/Entity/DocGeneratorTemplate.php b/src/Bundle/ChillDocGeneratorBundle/Entity/DocGeneratorTemplate.php index 2438e66d9..e2524e5ba 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Entity/DocGeneratorTemplate.php +++ b/src/Bundle/ChillDocGeneratorBundle/Entity/DocGeneratorTemplate.php @@ -69,7 +69,7 @@ class DocGeneratorTemplate * * @Serializer\Groups({"read"}) */ - private int $id; + private ?int $id = null; /** * @ORM\Column(type="json") diff --git a/src/Bundle/ChillDocGeneratorBundle/Repository/DocGeneratorTemplateRepository.php b/src/Bundle/ChillDocGeneratorBundle/Repository/DocGeneratorTemplateRepository.php index 0f2771b4e..b5f409032 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Repository/DocGeneratorTemplateRepository.php +++ b/src/Bundle/ChillDocGeneratorBundle/Repository/DocGeneratorTemplateRepository.php @@ -14,10 +14,9 @@ namespace Chill\DocGeneratorBundle\Repository; use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; -use Doctrine\Persistence\ObjectRepository; use Symfony\Component\HttpFoundation\RequestStack; -final class DocGeneratorTemplateRepository implements ObjectRepository +final class DocGeneratorTemplateRepository implements DocGeneratorTemplateRepositoryInterface { private readonly EntityRepository $repository; diff --git a/src/Bundle/ChillDocGeneratorBundle/Repository/DocGeneratorTemplateRepositoryInterface.php b/src/Bundle/ChillDocGeneratorBundle/Repository/DocGeneratorTemplateRepositoryInterface.php new file mode 100644 index 000000000..e5071e76a --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/Repository/DocGeneratorTemplateRepositoryInterface.php @@ -0,0 +1,23 @@ + + */ +interface DocGeneratorTemplateRepositoryInterface extends ObjectRepository +{ + public function countByEntity(string $entity): int; +} diff --git a/src/Bundle/ChillDocGeneratorBundle/Resources/views/DocGeneratorTemplate/index.html.twig b/src/Bundle/ChillDocGeneratorBundle/Resources/views/DocGeneratorTemplate/index.html.twig index 1adb6872b..cfe071986 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Resources/views/DocGeneratorTemplate/index.html.twig +++ b/src/Bundle/ChillDocGeneratorBundle/Resources/views/DocGeneratorTemplate/index.html.twig @@ -1,5 +1,16 @@ {% extends '@ChillMain/CRUD/Admin/index.html.twig' %} +{% block js %} + {{ parent() }} + {{ encore_entry_script_tags('mod_document_action_buttons_group') }} +{% endblock %} + +{% block css %} + {{ parent() }} + {{ encore_entry_link_tags('mod_document_action_buttons_group') }} +{% endblock %} + + {% block admin_content %} {% embed '@ChillMain/CRUD/_index.html.twig' %} {% block table_entities_thead_tr %} @@ -11,6 +22,47 @@ {% endblock %} {% block table_entities_tbody %} + {% if entities|length == 0 %} +

    {{ 'docgen.Any template configured'|trans }}

    + {% else %} +
    + {% for entity in entities %} +
    +
    +
    +

    {{ entity.name|localize_translatable_string }}

    +
    +
    +
    +

    {{ contextManager.getContextByKey(entity.context).name|trans }}

    +
    +
    +
    +
      +
    • +
      + + + + + + +
      +
    • +
    • + {{ entity.file|chill_document_button_group('Template file', true) }} +
    • +
    • + +
    • +
    +
    +
    + {% endfor %} +
    + {% endif %} + + {% for entity in entities %} {{ entity.id }} @@ -18,7 +70,7 @@ {{ contextManager.getContextByKey(entity.context).name|trans }}
    - + @@ -27,7 +79,14 @@
    - +
      +
    • + {{ entity.file|chill_document_button_group('Template file', true, {small: true}) }} +
    • +
    • + +
    • +
    {% endfor %} diff --git a/src/Bundle/ChillDocGeneratorBundle/Resources/views/DocGeneratorTemplate/pick-context.html.twig b/src/Bundle/ChillDocGeneratorBundle/Resources/views/DocGeneratorTemplate/pick-context.html.twig index c2f4a9a95..f61725765 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Resources/views/DocGeneratorTemplate/pick-context.html.twig +++ b/src/Bundle/ChillDocGeneratorBundle/Resources/views/DocGeneratorTemplate/pick-context.html.twig @@ -6,18 +6,20 @@

    {{ block('title') }}

    -
    +
    {% for key, context in contexts %} -
    -
    +
    + -
    - {{ context.description|trans|nl2br }} +
    +
    + {{ context.description|trans|nl2br }} +
    {% endfor %} diff --git a/src/Bundle/ChillDocGeneratorBundle/Resources/views/Email/on_generation_failed_email.txt.twig b/src/Bundle/ChillDocGeneratorBundle/Resources/views/Email/on_generation_failed_email.txt.twig index c4ca7079d..594785bfc 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Resources/views/Email/on_generation_failed_email.txt.twig +++ b/src/Bundle/ChillDocGeneratorBundle/Resources/views/Email/on_generation_failed_email.txt.twig @@ -1,6 +1,6 @@ -{{ creator.label }}, +{% if creator is not same as null %}{{ creator.label }},{% endif %} -{{ 'docgen.failure_email.The generation of the document {template_name} failed'|trans({'{template_name}': template.name|localize_translatable_string}) }} +{{ 'docgen.failure_email.The generation of the document %template_name% failed'|trans({'%template_name%': template.name|localize_translatable_string}) }} {{ 'docgen.failure_email.Forward this email to your administrator for solving'|trans }} diff --git a/src/Bundle/ChillDocGeneratorBundle/Resources/views/Email/send_data_dump_to_admin.txt.twig b/src/Bundle/ChillDocGeneratorBundle/Resources/views/Email/send_data_dump_to_admin.txt.twig new file mode 100644 index 000000000..566bfbfa3 --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/Resources/views/Email/send_data_dump_to_admin.txt.twig @@ -0,0 +1,7 @@ +{{ 'docgen.data_dump_email.Dear'|trans }} + +{{ 'docgen.data_dump_email.data_dump_ready_and_link'|trans }} + +{{ link }} + +{{ 'docgen.data_dump_email.link_valid_until'|trans({validity: validity}) }} diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Generator/Generator.php b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/Generator.php index 6c9a6f219..702d7f114 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Service/Generator/Generator.php +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/Generator.php @@ -17,54 +17,88 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; use Chill\DocGeneratorBundle\GeneratorDriver\DriverInterface; use Chill\DocGeneratorBundle\GeneratorDriver\Exception\TemplateException; use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Exception\StoredObjectManagerException; use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; use Chill\MainBundle\Entity\User; -use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\ManagerRegistry; use Psr\Log\LoggerInterface; -use Symfony\Component\HttpFoundation\File\File; +use Symfony\Component\Yaml\Yaml; class Generator implements GeneratorInterface { private const LOG_PREFIX = '[docgen generator] '; - public function __construct(private readonly ContextManagerInterface $contextManager, private readonly DriverInterface $driver, private readonly EntityManagerInterface $entityManager, private readonly LoggerInterface $logger, private readonly StoredObjectManagerInterface $storedObjectManager) - { + public function __construct( + private readonly ContextManagerInterface $contextManager, + private readonly DriverInterface $driver, + private readonly ManagerRegistry $objectManagerRegistry, + private readonly LoggerInterface $logger, + private readonly StoredObjectManagerInterface $storedObjectManager + ) { + } + + public function generateDataDump( + DocGeneratorTemplate $template, + int $entityId, + array $contextGenerationDataNormalized, + StoredObject $destinationStoredObject, + User $creator, + bool $clearEntityManagerDuringProcess = true, + ): StoredObject { + return $this->generateFromTemplate( + $template, + $entityId, + $contextGenerationDataNormalized, + $destinationStoredObject, + $creator, + $clearEntityManagerDuringProcess, + true, + ); } - /** - * @template T of File|null - * @template B of bool - * - * @param B $isTest - * @param (B is true ? T : null) $testFile - * - * @psalm-return (B is true ? string : null) - * - * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface|\Throwable - */ public function generateDocFromTemplate( DocGeneratorTemplate $template, int $entityId, array $contextGenerationDataNormalized, - ?StoredObject $destinationStoredObject = null, - bool $isTest = false, - ?File $testFile = null, - ?User $creator = null - ): ?string { - if ($destinationStoredObject instanceof StoredObject && StoredObject::STATUS_PENDING !== $destinationStoredObject->getStatus()) { + StoredObject $destinationStoredObject, + User $creator, + bool $clearEntityManagerDuringProcess = true, + ): StoredObject { + return $this->generateFromTemplate( + $template, + $entityId, + $contextGenerationDataNormalized, + $destinationStoredObject, + $creator, + $clearEntityManagerDuringProcess, + false, + ); + } + + private function generateFromTemplate( + DocGeneratorTemplate $template, + int $entityId, + array $contextGenerationDataNormalized, + StoredObject $destinationStoredObject, + User $creator, + bool $clearEntityManagerDuringProcess = true, + bool $generateDumpOnly = false, + ): StoredObject { + if (StoredObject::STATUS_PENDING !== $destinationStoredObject->getStatus()) { $this->logger->info(self::LOG_PREFIX.'Aborting generation of an already generated document'); throw new ObjectReadyException(); } $this->logger->info(self::LOG_PREFIX.'Starting generation of a document', [ 'entity_id' => $entityId, - 'destination_stored_object' => null === $destinationStoredObject ? null : $destinationStoredObject->getId(), + 'destination_stored_object' => $destinationStoredObject->getId(), ]); $context = $this->contextManager->getContextByDocGeneratorTemplate($template); $entity = $this - ->entityManager + ->objectManagerRegistry + ->getManagerForClass($context->getEntityClass()) ->find($context->getEntityClass(), $entityId) ; @@ -82,17 +116,47 @@ class Generator implements GeneratorInterface $data = $context->getData($template, $entity, $contextGenerationDataNormalized); - $destinationStoredObjectId = $destinationStoredObject instanceof StoredObject ? $destinationStoredObject->getId() : null; - $this->entityManager->clear(); - gc_collect_cycles(); - if (null !== $destinationStoredObjectId) { - $destinationStoredObject = $this->entityManager->find(StoredObject::class, $destinationStoredObjectId); + $destinationStoredObjectId = $destinationStoredObject->getId(); + + if ($clearEntityManagerDuringProcess) { + // we clean the entity manager + $this->objectManagerRegistry->getManagerForClass($context->getEntityClass())?->clear(); + + // this will force php to clean the memory + gc_collect_cycles(); } - if ($isTest && ($testFile instanceof File)) { - $templateDecrypted = file_get_contents($testFile->getPathname()); - } else { + // as we potentially deleted the storedObject from memory, we have to restore it + $destinationStoredObject = $this->objectManagerRegistry + ->getManagerForClass(StoredObject::class) + ->find(StoredObject::class, $destinationStoredObjectId); + + if ($generateDumpOnly) { + $content = Yaml::dump($data, 6); + /* @var StoredObject $destinationStoredObject */ + $destinationStoredObject + ->setType('application/yaml') + ->setFilename(sprintf('%s_yaml', uniqid('doc_', true))) + ->setStatus(StoredObject::STATUS_READY) + ; + + try { + $this->storedObjectManager->write($destinationStoredObject, $content); + } catch (StoredObjectManagerException $e) { + $destinationStoredObject->addGenerationErrors($e->getMessage()); + + throw new GeneratorException([$e->getMessage()], $e); + } + + return $destinationStoredObject; + } + + try { $templateDecrypted = $this->storedObjectManager->read($template->getFile()); + } catch (StoredObjectManagerException $e) { + $destinationStoredObject->addGenerationErrors($e->getMessage()); + + throw new GeneratorException([$e->getMessage()], $e); } try { @@ -105,19 +169,10 @@ class Generator implements GeneratorInterface $template->getFile()->getFilename() ); } catch (TemplateException $e) { + $destinationStoredObject->addGenerationErrors(implode("\n", $e->getErrors())); throw new GeneratorException($e->getErrors(), $e); } - if (true === $isTest) { - $this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [ - 'is_test' => true, - 'entity_id' => $entityId, - 'destination_stored_object' => null === $destinationStoredObject ? null : $destinationStoredObject->getId(), - ]); - - return $generatedResource; - } - /* @var StoredObject $destinationStoredObject */ $destinationStoredObject ->setType($template->getFile()->getType()) @@ -125,15 +180,19 @@ class Generator implements GeneratorInterface ->setStatus(StoredObject::STATUS_READY) ; - $this->storedObjectManager->write($destinationStoredObject, $generatedResource); + try { + $this->storedObjectManager->write($destinationStoredObject, $generatedResource); + } catch (StoredObjectManagerException $e) { + $destinationStoredObject->addGenerationErrors($e->getMessage()); - $this->entityManager->flush(); + throw new GeneratorException([$e->getMessage()], $e); + } $this->logger->info(self::LOG_PREFIX.'Finished generation of a document', [ 'entity_id' => $entityId, 'destination_stored_object' => $destinationStoredObject->getId(), ]); - return null; + return $destinationStoredObject; } } diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorInterface.php b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorInterface.php index c4ff38ac5..e7db7ba53 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorInterface.php +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Generator/GeneratorInterface.php @@ -13,29 +13,48 @@ namespace Chill\DocGeneratorBundle\Service\Generator; use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Exception\StoredObjectManagerException; use Chill\MainBundle\Entity\User; -use Symfony\Component\HttpFoundation\File\File; interface GeneratorInterface { /** - * @template T of File|null - * @template B of bool + * Generate a document and store the document on disk. * - * @param B $isTest - * @param (B is true ? T : null) $testFile + * The given $destinationStoredObject will be updated with filename, status, and eventually errors will be stored + * into the object. The number of generation trial will also be incremented. * - * @psalm-return (B is true ? string : null) + * This process requires a huge amount of data. For this reason, the entity manager will be cleaned during the process, + * unless the paarameter `$clearEntityManagerDuringProcess` is set on false. * - * @throws \Symfony\Component\Serializer\Exception\ExceptionInterface|\Throwable + * As the entity manager might be cleaned, the new instance of the stored object will be returned by this method. + * + * Ensure to store change in the database after each generation trial (call `EntityManagerInterface::flush`). + * + * @phpstan-impure + * + * @param StoredObject $destinationStoredObject will be update with filename, status and incremented of generation trials + * + * @throws StoredObjectManagerException if unable to decrypt the template or store the document */ public function generateDocFromTemplate( DocGeneratorTemplate $template, int $entityId, array $contextGenerationDataNormalized, - ?StoredObject $destinationStoredObject = null, - bool $isTest = false, - ?File $testFile = null, - ?User $creator = null - ): ?string; + StoredObject $destinationStoredObject, + User $creator, + bool $clearEntityManagerDuringProcess = true, + ): StoredObject; + + /** + * Generate a data dump, and store it within the `$destinationStoredObject`. + */ + public function generateDataDump( + DocGeneratorTemplate $template, + int $entityId, + array $contextGenerationDataNormalized, + StoredObject $destinationStoredObject, + User $creator, + bool $clearEntityManagerDuringProcess = true, + ): StoredObject; } diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/OnAfterMessageHandledClearStoredObjectCache.php b/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/OnAfterMessageHandledClearStoredObjectCache.php new file mode 100644 index 000000000..a558f5fde --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/OnAfterMessageHandledClearStoredObjectCache.php @@ -0,0 +1,64 @@ + [ + ['afterHandling', 0], + ], + WorkerMessageFailedEvent::class => [ + ['afterFails', 0], + ], + ]; + } + + public function afterHandling(WorkerMessageHandledEvent $event): void + { + if ($event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) { + $this->clearStoredObjectCache(); + } + } + + public function afterFails(WorkerMessageFailedEvent $event): void + { + if ($event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) { + $this->clearStoredObjectCache(); + } + } + + private function clearStoredObjectCache(): void + { + $this->logger->debug('clear the cache after generation of a document'); + + $this->storedObjectManager->clearCache(); + } +} diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/OnGenerationFails.php b/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/OnGenerationFails.php index 57006cb9d..e1bd20ac8 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/OnGenerationFails.php +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/OnGenerationFails.php @@ -11,10 +11,11 @@ declare(strict_types=1); namespace Chill\DocGeneratorBundle\Service\Messenger; -use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository; +use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepositoryInterface; use Chill\DocGeneratorBundle\Service\Generator\GeneratorException; +use Chill\DocGeneratorBundle\tests\Service\Messenger\OnGenerationFailsTest; use Chill\DocStoreBundle\Entity\StoredObject; -use Chill\DocStoreBundle\Repository\StoredObjectRepository; +use Chill\DocStoreBundle\Repository\StoredObjectRepositoryInterface; use Chill\MainBundle\Repository\UserRepositoryInterface; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; @@ -24,12 +25,22 @@ use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; use Symfony\Contracts\Translation\TranslatorInterface; +/** + * @see OnGenerationFailsTest for test suite + */ final readonly class OnGenerationFails implements EventSubscriberInterface { public const LOG_PREFIX = '[docgen failed] '; - public function __construct(private DocGeneratorTemplateRepository $docGeneratorTemplateRepository, private EntityManagerInterface $entityManager, private LoggerInterface $logger, private MailerInterface $mailer, private StoredObjectRepository $storedObjectRepository, private TranslatorInterface $translator, private UserRepositoryInterface $userRepository) - { + public function __construct( + private DocGeneratorTemplateRepositoryInterface $docGeneratorTemplateRepository, + private EntityManagerInterface $entityManager, + private LoggerInterface $logger, + private MailerInterface $mailer, + private StoredObjectRepositoryInterface $storedObjectRepository, + private TranslatorInterface $translator, + private UserRepositoryInterface $userRepository + ) { } public static function getSubscribedEvents() @@ -45,13 +56,12 @@ final readonly class OnGenerationFails implements EventSubscriberInterface return; } - if (!$event->getEnvelope()->getMessage() instanceof RequestGenerationMessage) { + $message = $event->getEnvelope()->getMessage(); + + if (!$message instanceof RequestGenerationMessage) { return; } - /** @var RequestGenerationMessage $message */ - $message = $event->getEnvelope()->getMessage(); - $this->logger->error(self::LOG_PREFIX.'Docgen failed', [ 'stored_object_id' => $message->getDestinationStoredObjectId(), 'entity_id' => $message->getEntityId(), @@ -79,16 +89,8 @@ final readonly class OnGenerationFails implements EventSubscriberInterface private function warnCreator(RequestGenerationMessage $message, WorkerMessageFailedEvent $event): void { - $creatorId = $message->getCreatorId(); - - if (null === $creator = $this->userRepository->find($creatorId)) { - $this->logger->error(self::LOG_PREFIX.'Creator not found with given id', ['creator_id', $creatorId]); - - return; - } - - if (null === $creator->getEmail() || '' === $creator->getEmail()) { - $this->logger->info(self::LOG_PREFIX.'Creator does not have any email', ['user' => $creator->getUsernameCanonical()]); + if (null === $message->getSendResultToEmail() || '' === $message->getSendResultToEmail()) { + $this->logger->info(self::LOG_PREFIX.'No email associated with this request generation'); return; } @@ -96,7 +98,7 @@ final readonly class OnGenerationFails implements EventSubscriberInterface // if the exception is not a GeneratorException, we try the previous one... $throwable = $event->getThrowable(); if (!$throwable instanceof GeneratorException) { - $throwable = $throwable->getPrevious(); + $throwable = $throwable->getPrevious() ?? $throwable; } if ($throwable instanceof GeneratorException) { @@ -111,8 +113,14 @@ final readonly class OnGenerationFails implements EventSubscriberInterface return; } + if (null === $creator = $this->userRepository->find($message->getCreatorId())) { + $this->logger->error(self::LOG_PREFIX.'Creator not found'); + + return; + } + $email = (new TemplatedEmail()) - ->to($creator->getEmail()) + ->to($message->getSendResultToEmail()) ->subject($this->translator->trans('docgen.failure_email.The generation of a document failed')) ->textTemplate('@ChillDocGenerator/Email/on_generation_failed_email.txt.twig') ->context([ diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationHandler.php b/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationHandler.php index 4ec59d9d4..c20971f27 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationHandler.php +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationHandler.php @@ -11,15 +11,21 @@ declare(strict_types=1); namespace Chill\DocGeneratorBundle\Service\Messenger; +use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface; use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository; use Chill\DocGeneratorBundle\Service\Generator\Generator; +use Chill\DocGeneratorBundle\Service\Generator\GeneratorException; use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Exception\StoredObjectManagerException; use Chill\DocStoreBundle\Repository\StoredObjectRepository; use Chill\MainBundle\Repository\UserRepositoryInterface; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; +use Symfony\Bridge\Twig\Mime\TemplatedEmail; +use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * Handle the request of document generation. @@ -30,8 +36,17 @@ class RequestGenerationHandler implements MessageHandlerInterface private const LOG_PREFIX = '[docgen message handler] '; - public function __construct(private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository, private readonly EntityManagerInterface $entityManager, private readonly Generator $generator, private readonly LoggerInterface $logger, private readonly StoredObjectRepository $storedObjectRepository, private readonly UserRepositoryInterface $userRepository) - { + public function __construct( + private readonly DocGeneratorTemplateRepository $docGeneratorTemplateRepository, + private readonly EntityManagerInterface $entityManager, + private readonly Generator $generator, + private readonly LoggerInterface $logger, + private readonly StoredObjectRepository $storedObjectRepository, + private readonly UserRepositoryInterface $userRepository, + private readonly MailerInterface $mailer, + private readonly TempUrlGeneratorInterface $tempUrlGenerator, + private readonly TranslatorInterface $translator, + ) { } public function __invoke(RequestGenerationMessage $message) @@ -45,25 +60,59 @@ class RequestGenerationHandler implements MessageHandlerInterface } if ($destinationStoredObject->getGenerationTrialsCounter() >= self::AUTHORIZED_TRIALS) { + $this->logger->error(self::LOG_PREFIX.'Request generation abandoned: maximum number of retry reached', [ + 'template_id' => $message->getTemplateId(), + 'destination_stored_object' => $message->getDestinationStoredObjectId(), + 'trial' => $destinationStoredObject->getGenerationTrialsCounter(), + ]); + throw new UnrecoverableMessageHandlingException('maximum number of retry reached'); } $creator = $this->userRepository->find($message->getCreatorId()); + // we increase the number of generation trial in the object, and, in the same time, update the counter + // on the database side. This ensure that, if the script fails for any reason (memory limit reached), the + // counter is inscreased $destinationStoredObject->addGenerationTrial(); $this->entityManager->createQuery('UPDATE '.StoredObject::class.' s SET s.generationTrialsCounter = s.generationTrialsCounter + 1 WHERE s.id = :id') ->setParameter('id', $destinationStoredObject->getId()) ->execute(); - $this->generator->generateDocFromTemplate( - $template, - $message->getEntityId(), - $message->getContextGenerationData(), - $destinationStoredObject, - false, - null, - $creator - ); + try { + if ($message->isDumpOnly()) { + $destinationStoredObject = $this->generator->generateDataDump( + $template, + $message->getEntityId(), + $message->getContextGenerationData(), + $destinationStoredObject, + $creator + ); + + $this->sendDataDump($destinationStoredObject, $message); + } else { + $destinationStoredObject = $this->generator->generateDocFromTemplate( + $template, + $message->getEntityId(), + $message->getContextGenerationData(), + $destinationStoredObject, + $creator + ); + } + } catch (StoredObjectManagerException|GeneratorException $e) { + $this->entityManager->flush(); + + $this->logger->error(self::LOG_PREFIX.'Request generation failed', [ + 'template_id' => $message->getTemplateId(), + 'destination_stored_object' => $message->getDestinationStoredObjectId(), + 'trial' => $destinationStoredObject->getGenerationTrialsCounter(), + 'error' => $e->getTraceAsString(), + ]); + + throw $e; + } + + $this->entityManager->flush(); $this->logger->info(self::LOG_PREFIX.'Request generation finished', [ 'template_id' => $message->getTemplateId(), @@ -71,4 +120,23 @@ class RequestGenerationHandler implements MessageHandlerInterface 'duration_int' => (new \DateTimeImmutable('now'))->getTimestamp() - $message->getCreatedAt()->getTimestamp(), ]); } + + private function sendDataDump(StoredObject $destinationStoredObject, RequestGenerationMessage $message): void + { + $url = $this->tempUrlGenerator->generate('GET', $destinationStoredObject->getFilename(), 3600); + $parts = []; + parse_str(parse_url((string) $url->url)['query'], $parts); + $validity = \DateTimeImmutable::createFromFormat('U', $parts['temp_url_expires']); + + $email = (new TemplatedEmail()) + ->to($message->getSendResultToEmail()) + ->textTemplate('@ChillDocGenerator/Email/send_data_dump_to_admin.txt.twig') + ->context([ + 'link' => $url->url, + 'validity' => $validity, + ]) + ->subject($this->translator->trans('docgen.data_dump_email.subject')); + + $this->mailer->send($email); + } } diff --git a/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationMessage.php b/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationMessage.php index 092073817..d41485346 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationMessage.php +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Messenger/RequestGenerationMessage.php @@ -15,27 +15,33 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\MainBundle\Entity\User; -class RequestGenerationMessage +final readonly class RequestGenerationMessage { - private readonly int $creatorId; + private int $creatorId; - private readonly int $templateId; + private int $templateId; - private readonly int $destinationStoredObjectId; + private int $destinationStoredObjectId; - private readonly \DateTimeImmutable $createdAt; + private \DateTimeImmutable $createdAt; + + private ?string $sendResultToEmail; public function __construct( User $creator, DocGeneratorTemplate $template, - private readonly int $entityId, + private int $entityId, StoredObject $destinationStoredObject, - private readonly array $contextGenerationData + private array $contextGenerationData, + private bool $isTest = false, + ?string $sendResultToEmail = null, + private bool $dumpOnly = false, ) { $this->creatorId = $creator->getId(); $this->templateId = $template->getId(); $this->destinationStoredObjectId = $destinationStoredObject->getId(); $this->createdAt = new \DateTimeImmutable('now'); + $this->sendResultToEmail = $sendResultToEmail ?? $creator->getEmail(); } public function getCreatorId(): int @@ -67,4 +73,19 @@ class RequestGenerationMessage { return $this->createdAt; } + + public function isTest(): bool + { + return $this->isTest; + } + + public function getSendResultToEmail(): ?string + { + return $this->sendResultToEmail; + } + + public function isDumpOnly(): bool + { + return $this->dumpOnly; + } } diff --git a/src/Bundle/ChillDocGeneratorBundle/tests/Service/Context/Generator/GeneratorTest.php b/src/Bundle/ChillDocGeneratorBundle/tests/Service/Context/Generator/GeneratorTest.php index 2272f343e..0bb274228 100644 --- a/src/Bundle/ChillDocGeneratorBundle/tests/Service/Context/Generator/GeneratorTest.php +++ b/src/Bundle/ChillDocGeneratorBundle/tests/Service/Context/Generator/GeneratorTest.php @@ -20,7 +20,9 @@ use Chill\DocGeneratorBundle\Service\Generator\ObjectReadyException; use Chill\DocGeneratorBundle\Service\Generator\RelatedEntityNotFoundException; use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; +use Chill\MainBundle\Entity\User; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\ManagerRegistry; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -66,7 +68,11 @@ class GeneratorTest extends TestCase $entityManager->find('DummyClass', Argument::type('int')) ->willReturn($entity); $entityManager->clear()->shouldBeCalled(); - $entityManager->flush()->shouldBeCalled(); + $entityManager->flush()->shouldNotBeCalled(); + + $managerRegistry = $this->prophesize(ManagerRegistry::class); + $managerRegistry->getManagerForClass('DummyClass')->willReturn($entityManager->reveal()); + $managerRegistry->getManagerForClass(StoredObject::class)->willReturn($entityManager->reveal()); $storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class); $storedObjectManager->read($templateStoredObject)->willReturn('template'); @@ -75,7 +81,7 @@ class GeneratorTest extends TestCase $generator = new Generator( $contextManagerInterface->reveal(), $driver->reveal(), - $entityManager->reveal(), + $managerRegistry->reveal(), new NullLogger(), $storedObjectManager->reveal() ); @@ -84,7 +90,8 @@ class GeneratorTest extends TestCase $template, 1, [], - $destinationStoredObject + $destinationStoredObject, + new User() ); } @@ -95,7 +102,7 @@ class GeneratorTest extends TestCase $generator = new Generator( $this->prophesize(ContextManagerInterface::class)->reveal(), $this->prophesize(DriverInterface::class)->reveal(), - $this->prophesize(EntityManagerInterface::class)->reveal(), + $this->prophesize(ManagerRegistry::class)->reveal(), new NullLogger(), $this->prophesize(StoredObjectManagerInterface::class)->reveal() ); @@ -108,7 +115,8 @@ class GeneratorTest extends TestCase $template, 1, [], - $destinationStoredObject + $destinationStoredObject, + new User() ); } @@ -136,10 +144,14 @@ class GeneratorTest extends TestCase $entityManager->find(Argument::type('string'), Argument::type('int')) ->willReturn(null); + $managerRegistry = $this->prophesize(ManagerRegistry::class); + $managerRegistry->getManagerForClass('DummyClass')->willReturn($entityManager->reveal()); + $managerRegistry->getManagerForClass(StoredObject::class)->willReturn($entityManager->reveal()); + $generator = new Generator( $contextManagerInterface->reveal(), $this->prophesize(DriverInterface::class)->reveal(), - $entityManager->reveal(), + $managerRegistry->reveal(), new NullLogger(), $this->prophesize(StoredObjectManagerInterface::class)->reveal() ); @@ -148,7 +160,8 @@ class GeneratorTest extends TestCase $template, 1, [], - $destinationStoredObject + $destinationStoredObject, + new User() ); } } diff --git a/src/Bundle/ChillDocGeneratorBundle/tests/Service/Messenger/OnAfterMessageHandledClearStoredObjectCacheTest.php b/src/Bundle/ChillDocGeneratorBundle/tests/Service/Messenger/OnAfterMessageHandledClearStoredObjectCacheTest.php new file mode 100644 index 000000000..9996b9b86 --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/tests/Service/Messenger/OnAfterMessageHandledClearStoredObjectCacheTest.php @@ -0,0 +1,107 @@ +prophesize(StoredObjectManagerInterface::class); + $storedObjectManager->clearCache()->shouldNotBeCalled(); + + $eventSubscriber = $this->buildEventSubscriber($storedObjectManager->reveal()); + + $eventSubscriber->afterHandling($this->buildEventSuccess(new \stdClass())); + $eventSubscriber->afterFails($this->buildEventFailed(new \stdClass())); + } + + public function testThatConcernedEventCallAClearCache(): void + { + $storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class); + $storedObjectManager->clearCache()->shouldBeCalledTimes(2); + + $eventSubscriber = $this->buildEventSubscriber($storedObjectManager->reveal()); + + $eventSubscriber->afterHandling($this->buildEventSuccess($this->buildRequestGenerationMessage())); + $eventSubscriber->afterFails($this->buildEventFailed($this->buildRequestGenerationMessage())); + } + + private function buildRequestGenerationMessage( + ): RequestGenerationMessage { + $creator = new User(); + $creator->setEmail('fake@example.com'); + + $class = new \ReflectionClass($creator); + $property = $class->getProperty('id'); + $property->setAccessible(true); + $property->setValue($creator, 1); + + $template ??= new DocGeneratorTemplate(); + $class = new \ReflectionClass($template); + $property = $class->getProperty('id'); + $property->setAccessible(true); + $property->setValue($template, 2); + + $destinationStoredObject = new StoredObject(); + $class = new \ReflectionClass($destinationStoredObject); + $property = $class->getProperty('id'); + $property->setAccessible(true); + $property->setValue($destinationStoredObject, 3); + + return new RequestGenerationMessage( + $creator, + $template, + 1, + $destinationStoredObject, + [], + ); + } + + private function buildEventSubscriber(StoredObjectManagerInterface $storedObjectManager): OnAfterMessageHandledClearStoredObjectCache + { + return new OnAfterMessageHandledClearStoredObjectCache($storedObjectManager, new NullLogger()); + } + + private function buildEventFailed(object $message): WorkerMessageFailedEvent + { + $envelope = new Envelope($message); + + return new WorkerMessageFailedEvent($envelope, 'testing', new \RuntimeException()); + } + + private function buildEventSuccess(object $message): WorkerMessageHandledEvent + { + $envelope = new Envelope($message); + + return new WorkerMessageHandledEvent($envelope, 'test_receiver'); + } +} diff --git a/src/Bundle/ChillDocGeneratorBundle/tests/Service/Messenger/OnGenerationFailsTest.php b/src/Bundle/ChillDocGeneratorBundle/tests/Service/Messenger/OnGenerationFailsTest.php new file mode 100644 index 000000000..f96b03020 --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/tests/Service/Messenger/OnGenerationFailsTest.php @@ -0,0 +1,226 @@ +prophesize(EntityManagerInterface::class); + $entityManager->flush()->shouldNotBeCalled(); + + $mailer = $this->prophesize(MailerInterface::class); + $mailer->send()->shouldNotBeCalled(); + + $eventSubscriber = $this->buildOnGenerationFailsEventSubscriber( + entityManager: $entityManager->reveal(), + mailer: $mailer->reveal() + ); + + $event = $this->buildEvent(new \stdClass()); + + $eventSubscriber->onMessageFailed($event); + } + + public function testMessageThatWillBeRetriedAreNotHandled(): void + { + $storedObject = new StoredObject(); + + $entityManager = $this->prophesize(EntityManagerInterface::class); + $entityManager->flush()->shouldNotBeCalled(); + + $mailer = $this->prophesize(MailerInterface::class); + $mailer->send()->shouldNotBeCalled(); + + $eventSubscriber = $this->buildOnGenerationFailsEventSubscriber( + entityManager: $entityManager->reveal(), + mailer: $mailer->reveal() + ); + + $event = $this->buildEvent($this->buildRequestGenerationMessage($storedObject)); + $event->setForRetry(); + + $eventSubscriber->onMessageFailed($event); + } + + public function testThatANotRetriyableEventWillMarkObjectAsFailed(): void + { + $storedObject = new StoredObject(); + + $entityManager = $this->prophesize(EntityManagerInterface::class); + $entityManager->flush()->shouldBeCalled(); + + $mailer = $this->prophesize(MailerInterface::class); + $mailer->send(Argument::type(RawMessage::class), Argument::any())->shouldBeCalled(); + + $eventSubscriber = $this->buildOnGenerationFailsEventSubscriber( + entityManager: $entityManager->reveal(), + mailer: $mailer->reveal(), + storedObject: $storedObject + ); + + $event = $this->buildEvent($this->buildRequestGenerationMessage($storedObject)); + + $eventSubscriber->onMessageFailed($event); + + self::assertEquals(StoredObject::STATUS_FAILURE, $storedObject->getStatus()); + } + + public function testThatANonRetryableEventSendAnEmail(): void + { + $storedObject = new StoredObject(); + + $entityManager = $this->prophesize(EntityManagerInterface::class); + $entityManager->flush()->shouldBeCalled(); + + $mailer = $this->prophesize(MailerInterface::class); + $mailer->send( + Argument::that(function ($arg): bool { + if (!$arg instanceof Email) { + return false; + } + + foreach ($arg->getTo() as $to) { + if ('test@test.com' === $to->getAddress()) { + return true; + } + } + + return false; + }), + Argument::any() + ) + ->shouldBeCalled(); + + $eventSubscriber = $this->buildOnGenerationFailsEventSubscriber( + entityManager: $entityManager->reveal(), + mailer: $mailer->reveal(), + storedObject: $storedObject + ); + + $event = $this->buildEvent($this->buildRequestGenerationMessage($storedObject, sendResultToEmail: 'test@test.com')); + + $eventSubscriber->onMessageFailed($event); + } + + private function buildRequestGenerationMessage( + StoredObject $destinationStoredObject, + ?User $creator = null, + ?DocGeneratorTemplate $template = null, + array $contextGenerationData = [], + bool $isTest = false, + ?string $sendResultToEmail = null, + ): RequestGenerationMessage { + if (null === $creator) { + $creator = new User(); + $creator->setEmail('fake@example.com'); + } + + if (null === $creator->getId()) { + $class = new \ReflectionClass($creator); + $property = $class->getProperty('id'); + $property->setAccessible(true); + $property->setValue($creator, 1); + } + + $template ??= new DocGeneratorTemplate(); + $class = new \ReflectionClass($template); + $property = $class->getProperty('id'); + $property->setAccessible(true); + $property->setValue($template, 2); + + $class = new \ReflectionClass($destinationStoredObject); + $property = $class->getProperty('id'); + $property->setAccessible(true); + $property->setValue($destinationStoredObject, 3); + + return new RequestGenerationMessage( + $creator, + $template, + 1, + $destinationStoredObject, + $contextGenerationData, + $isTest, + $sendResultToEmail + ); + } + + private function buildOnGenerationFailsEventSubscriber( + ?StoredObject $storedObject = null, + ?EntityManagerInterface $entityManager = null, + ?MailerInterface $mailer = null, + ): OnGenerationFails { + $storedObjectRepository = $this->prophesize(StoredObjectRepositoryInterface::class); + $storedObjectRepository->find(Argument::type('int'))->willReturn($storedObject ?? new StoredObject()); + + if (null === $entityManager) { + $entityManagerProphecy = $this->prophesize(EntityManagerInterface::class); + } + + if (null === $mailer) { + $mailerProphecy = $this->prophesize(MailerInterface::class); + } + + $translator = $this->prophesize(TranslatorInterface::class); + $translator->trans(Argument::type('string'))->will(fn ($args) => $args[0]); + + $userRepository = $this->prophesize(UserRepositoryInterface::class); + $userRepository->find(Argument::type('int'))->willReturn(new User()); + + $docGeneratorTemplateRepository = $this->prophesize(DocGeneratorTemplateRepositoryInterface::class); + $docGeneratorTemplateRepository->find(Argument::type('int'))->willReturn(new DocGeneratorTemplate()); + + return new OnGenerationFails( + $docGeneratorTemplateRepository->reveal(), + $entityManager ?? $entityManagerProphecy->reveal(), + new NullLogger(), + $mailer ?? $mailerProphecy->reveal(), + $storedObjectRepository->reveal(), + $translator->reveal(), + $userRepository->reveal() + ); + } + + private function buildEvent(object $message): WorkerMessageFailedEvent + { + $envelope = new Envelope($message); + + return new WorkerMessageFailedEvent($envelope, 'testing', new \RuntimeException()); + } +} diff --git a/src/Bundle/ChillDocGeneratorBundle/translations/messages+intl-icu.fr.yml b/src/Bundle/ChillDocGeneratorBundle/translations/messages+intl-icu.fr.yml new file mode 100644 index 000000000..df20907c9 --- /dev/null +++ b/src/Bundle/ChillDocGeneratorBundle/translations/messages+intl-icu.fr.yml @@ -0,0 +1,4 @@ +docgen: + data_dump_email: + link_valid_until: >- + Ce lien est valide jusqu'au {validity, date, full}, {validity, time, medium} diff --git a/src/Bundle/ChillDocGeneratorBundle/translations/messages.fr.yml b/src/Bundle/ChillDocGeneratorBundle/translations/messages.fr.yml index 5e55d6df8..1fdf14c02 100644 --- a/src/Bundle/ChillDocGeneratorBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillDocGeneratorBundle/translations/messages.fr.yml @@ -14,13 +14,31 @@ docgen: Doc generation is pending: La génération de ce document est en cours Come back later: Revenir plus tard + Send report to: Envoyer le rapport à + Send report errors to this email address: Les rapports d'erreurs seront envoyés à l'adresse email indiquée + Generate as creator: Générer en tant que + The document will be generated as the given creator: Le document sera généré à la place de l'utilisateur indiqué + Show data instead of generating: Montrer les données au lieu de générer le document + + Any template configured: Aucun gabarit de document configuré + + entity_id_placeholder: Identifiant de l'entité + failure_email: The generation of a document failed: La génération d'un document a échoué - The generation of the document {template_name} failed: La génération d'un document à partir du modèle {{ template_name }} a échoué. + The generation of the document %template_name% failed: La génération d'un document à partir du modèle {{ template_name }} a échoué. The following errors were encoutered: Les erreurs suivantes ont été rencontrées Forward this email to your administrator for solving: Faites suivre ce message vers votre administrateur pour la résolution du problème. References: Références + data_dump_email: + subject: Contenu des données de génération de document disponible + Dear: Cher + data_dump_ready_and_link: >- + Le contenu des données est disponible. Vous pouvez le télécharger à l'aide du lien suivant: + + + crud: docgen_template: index: @@ -28,5 +46,4 @@ crud: add_new: Créer -Show data instead of generating: Montrer les données au lieu de générer le document Template file: Fichier modèle diff --git a/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php b/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php index 2ee60d80a..4a16d33f4 100644 --- a/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php +++ b/src/Bundle/ChillDocStoreBundle/Entity/StoredObject.php @@ -25,6 +25,11 @@ use Symfony\Component\Serializer\Annotation as Serializer; /** * Represent a document stored in an object store. * + * StoredObjects 's content should be read and written using the @see{StoredObjectManagerInterface}. + * + * The property `$deleteAt` allow a deletion of the document after the given date. But this property should + * be set before the document is actually written by the StoredObjectManager. + * * @ORM\Entity * * @ORM\Table("chill_doc.stored_object") @@ -117,6 +122,16 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa */ private int $generationTrialsCounter = 0; + /** + * @ORM\Column(type="datetime_immutable", nullable=true, options={"default": null}) + */ + private ?\DateTimeImmutable $deleteAt = null; + + /** + * @ORM\Column(type="text", nullable=false, options={"default": ""}) + */ + private string $generationErrors = ''; + /** * @param StoredObject::STATUS_* $status */ @@ -144,6 +159,11 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa */ public function getCreationDate(): \DateTime { + if (null === $this->createdAt) { + // this scenario will quite never happens + return new \DateTime('now'); + } + return \DateTime::createFromImmutable($this->createdAt); } @@ -303,4 +323,37 @@ class StoredObject implements AsyncFileInterface, Document, TrackCreationInterfa { return self::STATUS_FAILURE === $this->getStatus(); } + + public function getDeleteAt(): ?\DateTimeImmutable + { + return $this->deleteAt; + } + + public function setDeleteAt(?\DateTimeImmutable $deleteAt): StoredObject + { + $this->deleteAt = $deleteAt; + + return $this; + } + + public function getGenerationErrors(): string + { + return $this->generationErrors; + } + + /** + * Adds generation errors to the stored object. + * + * The existing generation errors are not removed + * + * @param string $generationErrors the generation errors to be added + * + * @return StoredObject the modified StoredObject instance + */ + public function addGenerationErrors(string $generationErrors): StoredObject + { + $this->generationErrors = $this->generationErrors.$generationErrors."\n"; + + return $this; + } } diff --git a/src/Bundle/ChillDocStoreBundle/Repository/StoredObjectRepository.php b/src/Bundle/ChillDocStoreBundle/Repository/StoredObjectRepository.php index d2e715f7e..84bc7d4cb 100644 --- a/src/Bundle/ChillDocStoreBundle/Repository/StoredObjectRepository.php +++ b/src/Bundle/ChillDocStoreBundle/Repository/StoredObjectRepository.php @@ -14,11 +14,10 @@ namespace Chill\DocStoreBundle\Repository; use Chill\DocStoreBundle\Entity\StoredObject; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; -use Doctrine\Persistence\ObjectRepository; -final class StoredObjectRepository implements ObjectRepository +final readonly class StoredObjectRepository implements StoredObjectRepositoryInterface { - private readonly EntityRepository $repository; + private EntityRepository $repository; public function __construct(EntityManagerInterface $entityManager) { diff --git a/src/Bundle/ChillDocStoreBundle/Repository/StoredObjectRepositoryInterface.php b/src/Bundle/ChillDocStoreBundle/Repository/StoredObjectRepositoryInterface.php new file mode 100644 index 000000000..df2202b4f --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Repository/StoredObjectRepositoryInterface.php @@ -0,0 +1,22 @@ + + */ +interface StoredObjectRepositoryInterface extends ObjectRepository +{ +} diff --git a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php index b55074627..b6ab798b8 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php +++ b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManager.php @@ -104,6 +104,12 @@ final class StoredObjectManager implements StoredObjectManagerInterface ) : $clearContent; + $headers = []; + + if (null !== $document->getDeleteAt()) { + $headers['X-Delete-At'] = $document->getDeleteAt()->getTimestamp(); + } + try { $response = $this ->client @@ -118,6 +124,7 @@ final class StoredObjectManager implements StoredObjectManagerInterface ->url, [ 'body' => $encryptedContent, + 'headers' => $headers, ] ); } catch (TransportExceptionInterface $exception) { @@ -129,6 +136,11 @@ final class StoredObjectManager implements StoredObjectManagerInterface } } + public function clearCache(): void + { + $this->inMemory = []; + } + private function extractLastModifiedFromResponse(ResponseInterface $response): \DateTimeImmutable { $lastModifiedString = (($response->getHeaders()['last-modified'] ?? [])[0] ?? ''); diff --git a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManagerInterface.php b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManagerInterface.php index cee6586ea..d55f68023 100644 --- a/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManagerInterface.php +++ b/src/Bundle/ChillDocStoreBundle/Service/StoredObjectManagerInterface.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\DocStoreBundle\Service; use Chill\DocStoreBundle\Entity\StoredObject; +use Chill\DocStoreBundle\Exception\StoredObjectManagerException; interface StoredObjectManagerInterface { @@ -23,6 +24,8 @@ interface StoredObjectManagerInterface * @param StoredObject $document the document * * @return string the retrieved content in clear + * + * @throws StoredObjectManagerException if unable to read or decrypt the content */ public function read(StoredObject $document): string; @@ -31,6 +34,10 @@ interface StoredObjectManagerInterface * * @param StoredObject $document the document * @param $clearContent The content to store in clear + * + * @throws StoredObjectManagerException */ public function write(StoredObject $document, string $clearContent): void; + + public function clearCache(): void; } diff --git a/src/Bundle/ChillDocStoreBundle/Tests/StoredObjectManagerTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Service/StoredObjectManagerTest.php similarity index 81% rename from src/Bundle/ChillDocStoreBundle/Tests/StoredObjectManagerTest.php rename to src/Bundle/ChillDocStoreBundle/Tests/Service/StoredObjectManagerTest.php index bb6971939..e00582367 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/StoredObjectManagerTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Service/StoredObjectManagerTest.php @@ -9,7 +9,7 @@ declare(strict_types=1); * the LICENSE file that was distributed with this source code. */ -namespace Chill\DocStoreBundle\Tests; +namespace Chill\DocStoreBundle\Tests\Service; use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface; use Chill\DocStoreBundle\Entity\StoredObject; @@ -117,6 +117,41 @@ final class StoredObjectManagerTest extends TestCase self::assertEquals($clearContent, $storedObjectManager->read($storedObject)); } + public function testWriteWithDeleteAt() + { + $storedObject = new StoredObject(); + + $expectedRequests = [ + function ($method, $url, $options): MockResponse { + self::assertEquals('PUT', $method); + self::assertArrayHasKey('headers', $options); + self::assertIsArray($options['headers']); + self::assertCount(0, array_filter($options['headers'], fn (string $header) => str_contains($header, 'X-Delete-At'))); + + return new MockResponse('', ['http_code' => 201]); + }, + + function ($method, $url, $options): MockResponse { + self::assertEquals('PUT', $method); + self::assertArrayHasKey('headers', $options); + self::assertIsArray($options['headers']); + self::assertCount(1, array_filter($options['headers'], fn (string $header) => str_contains($header, 'X-Delete-At'))); + self::assertContains('X-Delete-At: 1711014260', $options['headers']); + + return new MockResponse('', ['http_code' => 201]); + }, + ]; + $client = new MockHttpClient($expectedRequests); + + $manager = new StoredObjectManager($client, $this->getTempUrlGenerator($storedObject)); + + $manager->write($storedObject, 'ok'); + + // with a deletedAt date + $storedObject->setDeleteAt(\DateTimeImmutable::createFromFormat('U', '1711014260')); + $manager->write($storedObject, 'ok'); + } + private function getHttpClient(string $encodedContent): HttpClientInterface { $callback = static function ($method, $url, $options) use ($encodedContent) { diff --git a/src/Bundle/ChillDocStoreBundle/migrations/Version20240322100107.php b/src/Bundle/ChillDocStoreBundle/migrations/Version20240322100107.php new file mode 100644 index 000000000..5f7a92b48 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/migrations/Version20240322100107.php @@ -0,0 +1,36 @@ +addSql('ALTER TABLE chill_doc.stored_object ADD deleteAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_doc.stored_object ADD generationErrors TEXT DEFAULT \'\' NOT NULL'); + $this->addSql('COMMENT ON COLUMN chill_doc.stored_object.deleteAt IS \'(DC2Type:datetime_immutable)\''); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_doc.stored_object DROP deleteAt'); + $this->addSql('ALTER TABLE chill_doc.stored_object DROP generationErrors'); + } +} diff --git a/src/Bundle/ChillPersonBundle/Service/DocGenerator/AccompanyingPeriodContext.php b/src/Bundle/ChillPersonBundle/Service/DocGenerator/AccompanyingPeriodContext.php index 592ab21fb..b85c2ae70 100644 --- a/src/Bundle/ChillPersonBundle/Service/DocGenerator/AccompanyingPeriodContext.php +++ b/src/Bundle/ChillPersonBundle/Service/DocGenerator/AccompanyingPeriodContext.php @@ -227,7 +227,7 @@ class AccompanyingPeriodContext implements } } - if ($options['thirdParty']) { + if ($options['thirdParty'] ?? false) { $data['thirdParty'] = $this->normalizer->normalize($contextGenerationData['thirdParty'], 'docgen', [ 'docgen:expects' => ThirdParty::class, 'groups' => 'docgen:read', diff --git a/src/Bundle/ChillWopiBundle/src/Controller/Editor.php b/src/Bundle/ChillWopiBundle/src/Controller/Editor.php index feae8d708..172729d3f 100644 --- a/src/Bundle/ChillWopiBundle/src/Controller/Editor.php +++ b/src/Bundle/ChillWopiBundle/src/Controller/Editor.php @@ -15,7 +15,6 @@ use ChampsLibres\WopiLib\Contract\Service\Configuration\ConfigurationInterface; use ChampsLibres\WopiLib\Contract\Service\Discovery\DiscoveryInterface; use ChampsLibres\WopiLib\Contract\Service\DocumentManagerInterface; use Chill\DocStoreBundle\Entity\StoredObject; -use Chill\MainBundle\Entity\User; use Chill\WopiBundle\Service\Controller\ResponderInterface; use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; use loophp\psr17\Psr17Interface; @@ -43,13 +42,11 @@ final readonly class Editor public function __invoke(string $fileId, Request $request): Response { - if (null === $user = $this->security->getUser()) { + if (!($this->security->isGranted('ROLE_USER') || $this->security->isGranted('ROLE_ADMIN'))) { throw new AccessDeniedHttpException('Please authenticate to access this feature'); } - if (!$user instanceof User) { - throw new AccessDeniedHttpException('Please authenticate as a user to access this feature'); - } + $user = $this->security->getUser(); $configuration = $this->wopiConfiguration->jsonSerialize(); /** @var StoredObject $storedObject */ @@ -77,7 +74,12 @@ final readonly class Editor } if ([] === $discoverExtension = $this->wopiDiscovery->discoverMimeType($storedObject->getType())) { - throw new \Exception(sprintf('Unable to find mime type %s', $storedObject->getType())); + return new Response( + $this->engine + ->render('@ChillWopi/Editor/unable_to_edit_such_document.html.twig', [ + 'document' => $storedObject, + ]) + ); } $configuration['favIconUrl'] = ''; diff --git a/src/Bundle/ChillWopiBundle/src/Resources/views/Editor/unable_to_edit_such_document.html.twig b/src/Bundle/ChillWopiBundle/src/Resources/views/Editor/unable_to_edit_such_document.html.twig new file mode 100644 index 000000000..36b8cd631 --- /dev/null +++ b/src/Bundle/ChillWopiBundle/src/Resources/views/Editor/unable_to_edit_such_document.html.twig @@ -0,0 +1,34 @@ +{% extends '@ChillMain/layout.html.twig' %} + +{% block js %} + {{ parent() }} + {{ encore_entry_script_tags('mod_document_action_buttons_group') }} +{% endblock %} + +{% block css %} + {{ parent() }} + {{ encore_entry_link_tags('mod_document_action_buttons_group') }} +{% endblock %} + +{% block content %} +
    +
    +

    + {{ 'wopi_editor.document unsupported for edition'|trans }} +

    +
    + +
    +

    {{ document|chill_document_button_group(document.title|default('Document'), false) }}

    +
    + +
    + + +{% endblock content %} diff --git a/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php b/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php index 53e9ad819..0054bda4f 100644 --- a/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php +++ b/src/Bundle/ChillWopiBundle/src/Service/Wopi/AuthorizationManager.php @@ -38,7 +38,7 @@ class AuthorizationManager implements \ChampsLibres\WopiBundle\Contracts\Authori $user = $this->security->getUser(); - if (!$user instanceof User) { + if (!($user instanceof User || $this->security->isGranted('ROLE_ADMIN'))) { return false; } diff --git a/src/Bundle/ChillWopiBundle/src/Service/Wopi/UserManager.php b/src/Bundle/ChillWopiBundle/src/Service/Wopi/UserManager.php index 4a0857521..25a6f7c8d 100644 --- a/src/Bundle/ChillWopiBundle/src/Service/Wopi/UserManager.php +++ b/src/Bundle/ChillWopiBundle/src/Service/Wopi/UserManager.php @@ -25,6 +25,10 @@ class UserManager implements \ChampsLibres\WopiBundle\Contracts\UserManagerInter { $user = $this->security->getUser(); + if (!$user instanceof User && $this->security->isGranted('ROLE_ADMIN')) { + return $user->getUsername(); + } + if (!$user instanceof User) { return null; } @@ -36,6 +40,10 @@ class UserManager implements \ChampsLibres\WopiBundle\Contracts\UserManagerInter { $user = $this->security->getUser(); + if (!$user instanceof User && $this->security->isGranted('ROLE_ADMIN')) { + return $user->getUsername(); + } + if (!$user instanceof User) { return null; } diff --git a/src/Bundle/ChillWopiBundle/src/translations/messages.fr.yml b/src/Bundle/ChillWopiBundle/src/translations/messages.fr.yml new file mode 100644 index 000000000..2875550a1 --- /dev/null +++ b/src/Bundle/ChillWopiBundle/src/translations/messages.fr.yml @@ -0,0 +1,2 @@ +wopi_editor: + document unsupported for edition: Ce format de document n'est pas éditable From f3002631ea6f2b9ce832a1e9322a761de09a9b81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 26 Mar 2024 18:10:18 +0100 Subject: [PATCH 24/26] Release 2.18.0 --- .changes/unreleased/Feature-20240326-170418.yaml | 5 ----- .changes/unreleased/Fixed-20240320-123527.yaml | 6 ------ .changes/v2.18.0.md | 5 +++++ CHANGELOG.md | 16 ++++++++++++++++ 4 files changed, 21 insertions(+), 11 deletions(-) delete mode 100644 .changes/unreleased/Feature-20240326-170418.yaml delete mode 100644 .changes/unreleased/Fixed-20240320-123527.yaml create mode 100644 .changes/v2.18.0.md diff --git a/.changes/unreleased/Feature-20240326-170418.yaml b/.changes/unreleased/Feature-20240326-170418.yaml deleted file mode 100644 index f86f55d96..000000000 --- a/.changes/unreleased/Feature-20240326-170418.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Feature -body: Improve admin UX to configure document templates for document generation -time: 2024-03-26T17:04:18.351694753+01:00 -custom: - Issue: "268" diff --git a/.changes/unreleased/Fixed-20240320-123527.yaml b/.changes/unreleased/Fixed-20240320-123527.yaml deleted file mode 100644 index a9e13ae9e..000000000 --- a/.changes/unreleased/Fixed-20240320-123527.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: Fixed -body: 'Fix the join between job and user in the user list (admin): show only the current - user job' -time: 2024-03-20T12:35:27.885282599+01:00 -custom: - Issue: "267" diff --git a/.changes/v2.18.0.md b/.changes/v2.18.0.md new file mode 100644 index 000000000..ad52d1d87 --- /dev/null +++ b/.changes/v2.18.0.md @@ -0,0 +1,5 @@ +## v2.18.0 - 2024-03-26 +### Feature +* ([#268](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/268)) Improve admin UX to configure document templates for document generation +### Fixed +* ([#267](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/267)) Fix the join between job and user in the user list (admin): show only the current user job diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f5d91ddc..a022a870d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). +## v2.18.0 - 2024-03-26 +### Feature +* ([#268](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/268)) Improve admin UX to configure document templates for document generation +### Fixed +* ([#267](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/267)) Fix the join between job and user in the user list (admin): show only the current user job + +## v2.17.0 - 2024-03-19 +### Feature +* ([#237](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/237)) New export filter for social actions with an evaluation created between two dates +* ([#258](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/258)) In the list of accompangying period, add the list of person's centers and the duration of the course +* ([#238](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/238)) Allow to customize list person with new fields +* ([#159](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/159)) Admin can publish news on the homepage +### Fixed +* ([#264](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/264)) Fix languages: load the languages in all availables languages configured for Chill +* ([#259](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/259)) Keep a consistent behaviour between the filtering of activities within the document generation (model "accompanying period with activities"), and the same filter in the list of activities for an accompanying period + ## v2.16.3 - 2024-02-26 ### Fixed * ([#236](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/236)) Fix translation of user job -> 'service' must be 'métier' From 807f1b4aa169656b1a6a1ef350ffd6c55c5bb0eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 26 Mar 2024 22:08:01 +0100 Subject: [PATCH 25/26] Fix layout in admin document generation A layout issue in the admin document generation has been fixed, particularly in the ChillDocGeneratorBundle. Unnecessary elements such as table headers and multiple entity data rows in DocGeneratorTemplate have been removed, simplifying the view page and improving its performance. --- .../unreleased/Fixed-20240326-220740.yaml | 5 +++ .../DocGeneratorTemplate/index.html.twig | 33 ------------------- 2 files changed, 5 insertions(+), 33 deletions(-) create mode 100644 .changes/unreleased/Fixed-20240326-220740.yaml diff --git a/.changes/unreleased/Fixed-20240326-220740.yaml b/.changes/unreleased/Fixed-20240326-220740.yaml new file mode 100644 index 000000000..078631037 --- /dev/null +++ b/.changes/unreleased/Fixed-20240326-220740.yaml @@ -0,0 +1,5 @@ +kind: Fixed +body: Fix layout issue in document generation for admin (minor) +time: 2024-03-26T22:07:40.044924041+01:00 +custom: + Issue: "" diff --git a/src/Bundle/ChillDocGeneratorBundle/Resources/views/DocGeneratorTemplate/index.html.twig b/src/Bundle/ChillDocGeneratorBundle/Resources/views/DocGeneratorTemplate/index.html.twig index cfe071986..ee167bcf7 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Resources/views/DocGeneratorTemplate/index.html.twig +++ b/src/Bundle/ChillDocGeneratorBundle/Resources/views/DocGeneratorTemplate/index.html.twig @@ -14,11 +14,6 @@ {% block admin_content %} {% embed '@ChillMain/CRUD/_index.html.twig' %} {% block table_entities_thead_tr %} - - {{ 'Title'|trans }} - {{ 'docgen.Context'|trans }} - {{ 'docgen.test generate'|trans }} - {{ 'Edit'|trans }} {% endblock %} {% block table_entities_tbody %} @@ -62,34 +57,6 @@
    {% endif %} - - {% for entity in entities %} - - {{ entity.id }} - {{ entity.name|localize_translatable_string}} - {{ contextManager.getContextByKey(entity.context).name|trans }} - -
    - - - - - - -
    - - -
      -
    • - {{ entity.file|chill_document_button_group('Template file', true, {small: true}) }} -
    • -
    • - -
    • -
    - - - {% endfor %} {% endblock %} {% block actions_before %} From 8fd6986c47a3448ecd2ef194afdec2a53cbe5c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Tue, 26 Mar 2024 22:08:30 +0100 Subject: [PATCH 26/26] Release 2.18.1 --- .changes/unreleased/Fixed-20240326-220740.yaml | 5 ----- .changes/v2.18.1.md | 3 +++ CHANGELOG.md | 4 ++++ 3 files changed, 7 insertions(+), 5 deletions(-) delete mode 100644 .changes/unreleased/Fixed-20240326-220740.yaml create mode 100644 .changes/v2.18.1.md diff --git a/.changes/unreleased/Fixed-20240326-220740.yaml b/.changes/unreleased/Fixed-20240326-220740.yaml deleted file mode 100644 index 078631037..000000000 --- a/.changes/unreleased/Fixed-20240326-220740.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Fixed -body: Fix layout issue in document generation for admin (minor) -time: 2024-03-26T22:07:40.044924041+01:00 -custom: - Issue: "" diff --git a/.changes/v2.18.1.md b/.changes/v2.18.1.md new file mode 100644 index 000000000..f0d68925d --- /dev/null +++ b/.changes/v2.18.1.md @@ -0,0 +1,3 @@ +## v2.18.1 - 2024-03-26 +### Fixed +* Fix layout issue in document generation for admin (minor) diff --git a/CHANGELOG.md b/CHANGELOG.md index a022a870d..8c0a6b008 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). +## v2.18.1 - 2024-03-26 +### Fixed +* Fix layout issue in document generation for admin (minor) + ## v2.18.0 - 2024-03-26 ### Feature * ([#268](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/268)) Improve admin UX to configure document templates for document generation